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    }