001 /*
002 * Copyright (c) OSGi Alliance (2005, 2009). All Rights Reserved.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 * http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016
017 package org.osgi.service.event;
018
019 import java.util.Collections;
020 import java.util.Dictionary;
021 import java.util.Enumeration;
022 import java.util.HashMap;
023 import java.util.Iterator;
024 import java.util.Map;
025
026 import org.osgi.framework.Filter;
027
028 /**
029 * An event.
030 *
031 * <code>Event</code> objects are delivered to <code>EventHandler</code>
032 * services which subscribe to the topic of the event.
033 *
034 * @Immutable
035 * @version $Revision: 7003 $
036 */
037 public class Event {
038 /**
039 * The topic of this event.
040 */
041 private final String topic;
042 /**
043 * The properties carried by this event. Keys are strings and values are
044 * objects
045 */
046 private final Map /* <String,Object> */properties;
047
048 /**
049 * Constructs an event.
050 *
051 * @param topic The topic of the event.
052 * @param properties The event's properties (may be <code>null</code>). A
053 * property whose key is not of type <code>String</code> will be
054 * ignored.
055 * @throws IllegalArgumentException If topic is not a valid topic name.
056 * @since 1.2
057 */
058 public Event(String topic, Map/* <String,Object> */properties) {
059 validateTopicName(topic);
060 this.topic = topic;
061 int size = (properties == null) ? 1 : (properties.size() + 1);
062 Map p = new HashMap(size);
063 if (properties != null) {
064 for (Iterator iter = properties.keySet().iterator(); iter.hasNext();) {
065 Object key = iter.next();
066 if (key instanceof String) {
067 Object value = properties.get(key);
068 p.put(key, value);
069 }
070 }
071 }
072 p.put(EventConstants.EVENT_TOPIC, topic);
073 this.properties = p; // safely publish the map
074 }
075
076 /**
077 * Constructs an event.
078 *
079 * @param topic The topic of the event.
080 * @param properties The event's properties (may be <code>null</code>). A
081 * property whose key is not of type <code>String</code> will be
082 * ignored.
083 * @throws IllegalArgumentException If topic is not a valid topic name.
084 */
085 public Event(String topic, Dictionary/* <String,Object> */properties) {
086 validateTopicName(topic);
087 this.topic = topic;
088 int size = (properties == null) ? 1 : (properties.size() + 1);
089 Map p = new HashMap(size);
090 if (properties != null) {
091 for (Enumeration e = properties.keys(); e.hasMoreElements();) {
092 Object key = e.nextElement();
093 if (key instanceof String) {
094 Object value = properties.get(key);
095 p.put(key, value);
096 }
097 }
098 }
099 p.put(EventConstants.EVENT_TOPIC, topic);
100 this.properties = p; // safely publish the map
101 }
102
103 /**
104 * Retrieves a property.
105 *
106 * @param name the name of the property to retrieve
107 * @return The value of the property, or <code>null</code> if not found.
108 */
109 public final Object getProperty(String name) {
110 return properties.get(name);
111 }
112
113 /**
114 * Returns a list of this event's property names.
115 *
116 * @return A non-empty array with one element per property.
117 */
118 public final String[] getPropertyNames() {
119 return (String[]) properties.keySet().toArray(
120 new String[properties.size()]);
121 }
122
123 /**
124 * Returns the topic of this event.
125 *
126 * @return The topic of this event.
127 */
128 public final String getTopic() {
129 return topic;
130 }
131
132 /**
133 * Tests this event's properties against the given filter using a case
134 * sensitive match.
135 *
136 * @param filter The filter to test.
137 * @return true If this event's properties match the filter, false
138 * otherwise.
139 */
140 public final boolean matches(Filter filter) {
141 return filter.matchCase(new UnmodifiableDictionary(properties));
142 }
143
144 /**
145 * Compares this <code>Event</code> object to another object.
146 *
147 * <p>
148 * An event is considered to be <b>equal to</b> another event if the topic
149 * is equal and the properties are equal.
150 *
151 * @param object The <code>Event</code> object to be compared.
152 * @return <code>true</code> if <code>object</code> is a <code>Event</code>
153 * and is equal to this object; <code>false</code> otherwise.
154 */
155 public boolean equals(Object object) {
156 if (object == this) { // quick test
157 return true;
158 }
159
160 if (!(object instanceof Event)) {
161 return false;
162 }
163
164 Event event = (Event) object;
165 return topic.equals(event.topic) && properties.equals(event.properties);
166 }
167
168 /**
169 * Returns a hash code value for the object.
170 *
171 * @return An integer which is a hash code value for this object.
172 */
173 public int hashCode() {
174 int h = 31 * 17 + topic.hashCode();
175 h = 31 * h + properties.hashCode();
176 return h;
177 }
178
179 /**
180 * Returns the string representation of this event.
181 *
182 * @return The string representation of this event.
183 */
184 public String toString() {
185 return getClass().getName() + " [topic=" + topic + "]";
186 }
187
188 /**
189 * Called by the constructor to validate the topic name.
190 *
191 * @param topic The topic name to validate.
192 * @throws IllegalArgumentException If the topic name is invalid.
193 */
194 private static void validateTopicName(String topic) {
195 char[] chars = topic.toCharArray();
196 int length = chars.length;
197 if (length == 0) {
198 throw new IllegalArgumentException("empty topic");
199 }
200 for (int i = 0; i < length; i++) {
201 char ch = chars[i];
202 if (ch == '/') {
203 // Can't start or end with a '/' but anywhere else is okay
204 if (i == 0 || (i == length - 1)) {
205 throw new IllegalArgumentException(
206 "invalid topic: "
207 + topic);
208 }
209 // Can't have "//" as that implies empty token
210 if (chars[i-1] == '/') {
211 throw new IllegalArgumentException(
212 "invalid topic: "
213 + topic);
214 }
215 continue;
216 }
217 if (('A' <= ch) && (ch <= 'Z')) {
218 continue;
219 }
220 if (('a' <= ch) && (ch <= 'z')) {
221 continue;
222 }
223 if (('0' <= ch) && (ch <= '9')) {
224 continue;
225 }
226 if ((ch == '_') || (ch == '-')) {
227 continue;
228 }
229 throw new IllegalArgumentException("invalid topic: " + topic);
230 }
231 }
232
233 /**
234 * Unmodifiable wrapper for Dictionary.
235 */
236 private static class UnmodifiableDictionary extends Dictionary {
237 private final Map wrapped;
238 UnmodifiableDictionary(Map wrapped) {
239 this.wrapped = wrapped;
240 }
241 public Enumeration elements() {
242 return Collections.enumeration(wrapped.values());
243 }
244 public Object get(Object key) {
245 return wrapped.get(key);
246 }
247 public boolean isEmpty() {
248 return wrapped.isEmpty();
249 }
250 public Enumeration keys() {
251 return Collections.enumeration(wrapped.keySet());
252 }
253 public Object put(Object key, Object value) {
254 throw new UnsupportedOperationException();
255 }
256 public Object remove(Object key) {
257 throw new UnsupportedOperationException();
258 }
259 public int size() {
260 return wrapped.size();
261 }
262 }
263 }