View Javadoc

1   /* This file is part of COPAL (COntext Provisioning for All).
2    *
3    * COPAL is a part of SM4All (Smart hoMes for All) project.
4    *
5    * COPAL is free software: you can redistribute it and/or modify
6    * it under the terms of the GNU Lesser General Public License as published by
7    * the Free Software Foundation, either version 3 of the License, or
8    * (at your option) any later version.
9    *
10   * COPAL is distributed in the hope that it will be useful,
11   * but WITHOUT ANY WARRANTY; without even the implied warranty of
12   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13   * GNU Lesser General Public License for more details.
14   *
15   * You should have received a copy of the GNU Lesser General Public License
16   * along with COPAL. If not, see <http://www.gnu.org/licenses/>.
17   */
18  package at.ac.tuwien.infosys.sm4all.copal.api;
19  
20  import java.text.MessageFormat;
21  import java.util.Date;
22  import java.util.HashMap;
23  import java.util.LinkedList;
24  import java.util.List;
25  import java.util.Map;
26  import java.util.concurrent.CopyOnWriteArrayList;
27  import java.util.concurrent.atomic.AtomicBoolean;
28  import java.util.concurrent.locks.ReadWriteLock;
29  import java.util.concurrent.locks.ReentrantReadWriteLock;
30  import org.apache.log4j.Logger;
31  import org.w3c.dom.Document;
32  import at.ac.tuwien.infosys.sm4all.copal.api.event.ContextEvent;
33  import at.ac.tuwien.infosys.sm4all.copal.api.event.ContextEventType;
34  import at.ac.tuwien.infosys.sm4all.copal.api.event.xml.ParsingException;
35  import at.ac.tuwien.infosys.sm4all.copal.api.event.xml.SourceID;
36  import at.ac.tuwien.infosys.sm4all.copal.api.event.xml.TimeStamp;
37  import at.ac.tuwien.infosys.sm4all.copal.api.event.xml.TimeToLive;
38  
39  /**
40   * This class is used by the {@link ContextListener}s to tell on which
41   * {@link ContextEvent}s the listeners are interested in. Two properties are
42   * used to select {@link ContextEvent}s:
43   * <ul>
44   * <li>
45   * <code>eventName</code> - the name of the event (see
46   * {@link ContextEventType#getName()})</li>
47   * <li><code>criteria</code> - the logical expression using event's properties
48   * to further separate events of interest</li>
49   * </ul>
50   * 
51   * @author fei
52   * @author sanjin
53   */
54  public class ContextQuery {
55  
56      private static final ContextListener[] EMPTY_LISTENERS_ARRAY = new ContextListener[0];
57      private static final Logger LOGGER = Logger.getLogger(ContextQuery.class);
58  
59      private final List<ContextQueryObserver> observers = new CopyOnWriteArrayList<ContextQueryObserver>();
60      // key=listener name, value=listener
61      private final Map<String, ContextListener> listeners = new HashMap<String, ContextListener>();
62      private final ReadWriteLock lock = new ReentrantReadWriteLock();
63      private final String name;
64      private final String eventType;
65      private final String criteria;
66      private final AtomicBoolean destroyed = new AtomicBoolean(false);
67  
68      /**
69       * Creates an instance of {@link ContextQuery}. The <code>name</code> should
70       * be globally unique, meaning that if two {@link ContextQuery}s have same
71       * <code>name</code> then their <code>eventType</code>s and
72       * <code>criteria</code>s are equal. The <code>eventType</code> is name of
73       * {@link ContextEventType} on which <code>criteria</code> is executed. The
74       * <code>criteria</code> is the logical statement which further separates
75       * the events of interest. If {@link ContextQuery} should catch all
76       * <code>eventType</code> events, we set <code>criteria</code> to be
77       * <code>null</code> or blank string or use
78       * {@link ContextQuery#ContextQuery(String, String) ContextQuery(String,
79       * String)} constructor.
80       * 
81       * @param name the globally unique name of the {@link ContextQuery}.
82       * @param eventType the name of {@link ContextEventType}.
83       * @param criteria the logical expression of the {@link ContextQuery}.
84       * @throws NullPointerException if specified name or event type is
85       *         <code>null</code>.
86       * @throws IllegalArgumentException if specified name or event type is an
87       *         empty or blank string.
88       */
89      public ContextQuery(final String name, final String eventType,
90              final String criteria) {
91          super();
92  
93          if (name == null)
94              throw new NullPointerException("Query's name cannot be null.");
95          if (name.trim().isEmpty())
96              throw new IllegalArgumentException(
97                      "Query's name cannot be blank string.");
98          if (eventType == null)
99              throw new NullPointerException("Query's event type cannot be null.");
100         if (eventType.trim().isEmpty())
101             throw new IllegalArgumentException(
102                     "Query's event type cannot be blank string.");
103 
104         this.name = name;
105         this.eventType = eventType;
106         if ((criteria == null) || (criteria.trim().isEmpty()))
107             this.criteria = null;
108         else
109             this.criteria = criteria;
110 
111         if (LOGGER.isDebugEnabled())
112             LOGGER.debug(MessageFormat.format("Created query ''{0}''.",
113                     this.name));
114     }
115 
116     /**
117      * Create instance of the {@link ContextQuery}. The <code>name</code> should
118      * be globally unique, meaning that if two {@link ContextQuery}s have same
119      * <code>name</code> then their <code>eventType</code>s and
120      * <code>criteria</code>s are equal. The <code>eventType</code> is name of
121      * {@link ContextEventType} to be caught.
122      * 
123      * @param name the globally unique name of the {@link ContextQuery}.
124      * @param eventType the name of {@link ContextEventType}.
125      * @throws NullPointerException if specified name or event type is
126      *         <code>null</code>.
127      * @throws IllegalArgumentException if specified name or event type is an
128      *         empty or blank string.
129      */
130     public ContextQuery(final String name, final String eventType) {
131         this(name, eventType, null);
132     }
133 
134     /**
135      * @return the globally unique name of the {@link ContextQuery}.
136      */
137     public String getName() {
138         return this.name;
139     }
140 
141     /**
142      * @return the name of {@link ContextEventType}.
143      * @see ContextEventType#getName()
144      */
145     public String getEventType() {
146         return this.eventType;
147     }
148 
149     /**
150      * @return the logical expression the {@link ContextQuery}.
151      */
152     public String getCriteria() {
153         return this.criteria;
154     }
155 
156     /**
157      * @return if this {@link ContextQuery} has the criteria defined.
158      */
159     public boolean hasCriteria() {
160         return this.criteria != null;
161     }
162 
163     /**
164      * @return if this {@link ContextQuery} is destroyed.
165      */
166     public boolean isDestroyed() {
167         return this.destroyed.get();
168     }
169 
170     /**
171      * Register specified {@link ContextListener} with this {@link ContextQuery}
172      * . If event is caught, for which the criteria evaluates to
173      * <code>true</code>, the specified {@link ContextListener} will be invoked.
174      * 
175      * @param listener the {@link ContextListener}.
176      * @throws ListenerAlreadyRegisteredException if the {@link ContextListener}
177      *         with same name is already registered with this
178      *         {@link ContextQuery}.
179      * @throws NullPointerException if specified {@link ContextListener} is
180      *         <code>null</code>.
181      * @throws QueryDestroyedException if {@link ContextQuery} has been
182      *         previously destroyed.
183      */
184     public void register(final ContextListener listener)
185             throws ListenerAlreadyRegisteredException, QueryDestroyedException {
186         if (this.destroyed.get())
187             throw new QueryDestroyedException(this);
188         if (listener == null)
189             throw new NullPointerException("Listener cannot be null.");
190 
191         final String listenerName = listener.getName();
192 
193         this.lock.writeLock().lock();
194         try {
195             if (this.listeners.containsKey(listenerName))
196                 throw new ListenerAlreadyRegisteredException(listener);
197 
198             this.listeners.put(listenerName, listener);
199 
200             if (LOGGER.isInfoEnabled())
201                 LOGGER.info(MessageFormat.format(
202                         "Successfully registered listener ''{0}'' with ''{1}'' query.",
203                         listenerName, this.name));
204         } finally {
205             this.lock.writeLock().unlock();
206         }
207     }
208 
209     /**
210      * Unregister {@link ContextListener} with specified name from this
211      * {@link ContextQuery}. The {@link ContextListener} will not receive any
212      * further events from this {@link ContextQuery}.
213      * 
214      * @param listenerName the name of the {@link ContextListener}.
215      * @throws ListenerNotRegisteredException if the {@link ContextListener}
216      *         with specified name is not registered with this
217      *         {@link ContextQuery}.
218      * @throws NullPointerException if specified name of the
219      *         {@link ContextListener} is <code>null</code>.
220      */
221     public void unregister(final String listenerName)
222             throws ListenerNotRegisteredException {
223         if (listenerName == null)
224             throw new NullPointerException("Listener name cannot be null.");
225 
226         this.lock.writeLock().lock();
227         try {
228             if (!this.listeners.containsKey(listenerName))
229                 throw new ListenerNotRegisteredException(listenerName);
230 
231             if (!this.destroyed.get())
232                 this.listeners.remove(listenerName);
233 
234             if (LOGGER.isInfoEnabled())
235                 LOGGER.info(MessageFormat.format(
236                         "Successfully unregistered listener ''{0}'' from ''{1}'' query.",
237                         listenerName, this.name));
238         } finally {
239             this.lock.writeLock().unlock();
240         }
241     }
242 
243     /**
244      * Unregister all registered {@link ContextListener}s from this
245      * {@link ContextQuery}. The {@link ContextListener}s will not receive any
246      * further events from this {@link ContextQuery}.
247      */
248     public void unregisterAll() {
249         if (LOGGER.isDebugEnabled())
250             LOGGER.debug(MessageFormat.format(
251                     "Unregistering all context listeners from ''{0}'' query.",
252                     this.name));
253 
254         this.lock.writeLock().lock();
255         try {
256             for (final String listenerName : this.listeners.keySet())
257                 try {
258                     unregister(listenerName);
259                 } catch (final ListenerNotRegisteredException ex) {
260                     LOGGER.error(
261                             MessageFormat.format(
262                                     "Failed to unregister listener ''{0}'' from ''{1}'' query!",
263                                     listenerName, this.name), ex);
264                 }
265             this.listeners.clear();
266         } finally {
267             this.lock.writeLock().unlock();
268         }
269 
270         if (LOGGER.isInfoEnabled())
271             LOGGER.info(MessageFormat.format(
272                     "All context listeners from ''{0}'' query unregistered!",
273                     this.name));
274     }
275 
276     /**
277      * Destroys this {@link ContextQuery}. All registered
278      * {@link ContextListener}s will be unregistered and any future registration
279      * of a {@link ContextListener} and receiving of events will not be
280      * possible.
281      * 
282      * @throws QueryDestroyedException if {@link ContextQuery} has been already
283      *         destroyed.
284      */
285     public void destroy() throws QueryDestroyedException {
286         if (this.destroyed.getAndSet(true))
287             throw new QueryDestroyedException(this);
288 
289         this.lock.writeLock().lock();
290         try {
291             if (LOGGER.isDebugEnabled())
292                 LOGGER.debug(MessageFormat.format("Destroying ''{0}'' query.",
293                         this.name));
294 
295             notifyDestroy();
296             unregisterAll();
297             detachAll();
298 
299             if (LOGGER.isInfoEnabled())
300                 LOGGER.info(MessageFormat.format("''{0}'' query destroyed!",
301                         this.name));
302         } finally {
303             this.lock.writeLock().unlock();
304         }
305     }
306 
307     /**
308      * This method is called when a context event with same event name occurs
309      * for which the criteria is satisfied. This method in return calls all
310      * registered {@link ContextListener}s with specified event.
311      * 
312      * @param event occurred event.
313      * @throws QueryDestroyedException if {@link ContextQuery} has been
314      *         previously destroyed.
315      */
316     public void onEvent(final Document event) throws QueryDestroyedException {
317         if (this.destroyed.get())
318             throw new QueryDestroyedException(this);
319         if (event == null)
320             throw new NullPointerException("Event cannot be null.");
321 
322         final ContextListener[] currentListeners;
323         this.lock.readLock().lock();
324         try {
325             currentListeners = new ContextListener[this.listeners.size()];
326             this.listeners.values().toArray(currentListeners);
327         } finally {
328             this.lock.readLock().unlock();
329         }
330 
331         try {
332             final Date timeStamp = TimeStamp.INSTANCE.retrieve(event);
333             final long ttl = TimeToLive.INSTANCE.retrieve(event);
334             final Date lastCreationTime = new Date(System.currentTimeMillis()
335                     - ttl);
336             if (lastCreationTime.after(timeStamp)) {
337                 final String sourceID = SourceID.INSTANCE.retrieve(event);
338                 LOGGER.warn(MessageFormat.format(
339                         "Dropped stale event ''{0}'' from ''{1}''!",
340                         this.eventType, sourceID));
341             } else
342                 for (final ContextListener listener : currentListeners)
343                     listener.onEvent(event);
344         } catch (final ParsingException ex) {
345             LOGGER.error("Could not parse the event!", ex);
346         }
347     }
348 
349     /**
350      * Checks if a {@link ContextListener} with specified name is currently
351      * registered.
352      * 
353      * @param listenerName the name of the {@link ContextListener}.
354      * @return <code>true</code>> if such {@link ContextListener} is registered;
355      *         <code>false</code> otherwise.
356      */
357     public boolean isRegistered(final String listenerName) {
358         final boolean result;
359 
360         this.lock.readLock().lock();
361         try {
362             result = this.listeners.containsKey(listenerName);
363         } finally {
364             this.lock.readLock().unlock();
365         }
366 
367         return result;
368     }
369 
370     /**
371      * @return all {@link ContextListener}s which are registered with this
372      *         query.
373      */
374     public ContextListener[] getListeners() {
375         ContextListener[] result = EMPTY_LISTENERS_ARRAY;
376 
377         this.lock.readLock().lock();
378         try {
379             if (!this.listeners.isEmpty()) {
380                 result = new ContextListener[this.listeners.size()];
381                 result = this.listeners.values().toArray(result);
382             }
383         } finally {
384             this.lock.readLock().unlock();
385         }
386 
387         return result;
388     }
389 
390     /**
391      * Attach specified {@link ContextQueryObserver} so it will in future
392      * receive notifications of this {@link ContextQuery} state changes.
393      * 
394      * @param observer the {@link ContextQueryObserver}.
395      */
396     public void attach(final ContextQueryObserver observer) {
397         if (observer == null)
398             throw new NullPointerException("Query observer cannot be null.");
399 
400         this.observers.add(observer);
401     }
402 
403     /**
404      * Detach specified {@link ContextQueryObserver} so it will not receive any
405      * future notifications of this {@link ContextQuery} state changes.
406      * 
407      * @param observer the {@link ContextQueryObserver}.
408      */
409     public void detach(final ContextQueryObserver observer) {
410         if (observer == null)
411             throw new NullPointerException("Query observer cannot be null.");
412 
413         if (!this.destroyed.get())
414             this.observers.remove(observer);
415     }
416 
417     /**
418      * Detach all {@link ContextQueryObserver}s.
419      */
420     public void detachAll() {
421         if (LOGGER.isDebugEnabled())
422             LOGGER.debug(MessageFormat.format(
423                     "Detaching all observers of ''{0}'' query.", this.name));
424 
425         for (final ContextQueryObserver observer : new LinkedList<ContextQueryObserver>(
426                 this.observers))
427             detach(observer);
428 
429         this.observers.clear();
430 
431         if (LOGGER.isInfoEnabled())
432             LOGGER.info(MessageFormat.format(
433                     "All observers of ''{0}'' query detached.", this.name));
434     }
435 
436     /**
437      * Returns hash code for this {@link ContextQuery}. The hash code for a
438      * {@link ContextQuery} object is hash code of its name.
439      * 
440      * @return a hash code value for this {@link ContextQuery}.
441      */
442     @Override
443     public int hashCode() {
444         return this.name.hashCode();
445     }
446 
447     /**
448      * Compares this {@link ContextQuery} to the specified {@link Object}. The
449      * result is <code>true</code> if and only if the argument is not
450      * <code>null</code> and is a {@link ContextQuery} object that has same name
451      * as this {@link ContextQuery}.
452      * 
453      * @param obj the {@link Object} to compare this {@link ContextQuery}
454      *        against.
455      * @return <code>true</code> if {@link ContextQuery}s are equal;
456      *         <code>false</code> otherwise.
457      */
458     @Override
459     public boolean equals(final Object obj) {
460         boolean result = false;
461 
462         if (obj != null)
463             if (this == obj)
464                 result = true;
465             else if (this.getClass() == obj.getClass()) {
466                 final ContextQuery other = (ContextQuery) obj;
467 
468                 result = this.name.equals(other.name);
469             }
470 
471         return result;
472     }
473 
474     private void notifyDestroy() {
475         for (final ContextQueryObserver observer : this.observers)
476             observer.onDestroy(this);
477     }
478 }