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.query;
19  
20  import java.text.MessageFormat;
21  import java.util.Collection;
22  import java.util.HashMap;
23  import java.util.Map;
24  import java.util.concurrent.atomic.AtomicReference;
25  import java.util.concurrent.locks.ReadWriteLock;
26  import java.util.concurrent.locks.ReentrantReadWriteLock;
27  import org.apache.log4j.Level;
28  import org.apache.log4j.Logger;
29  import org.osgi.framework.BundleContext;
30  import at.ac.tuwien.infosys.sm4all.copal.api.event.ContextEvent;
31  import at.ac.tuwien.infosys.sm4all.copal.api.event.ContextEventAction;
32  import at.ac.tuwien.infosys.sm4all.copal.api.event.ContextEventType;
33  import at.ac.tuwien.infosys.sm4all.copal.api.listener.ContextListener;
34  import at.ac.tuwien.infosys.sm4all.copal.api.osgi.Tracker;
35  import at.ac.tuwien.infosys.sm4all.copal.api.service.ContextSecurityManager;
36  import at.ac.tuwien.infosys.sm4all.copal.api.util.AlreadyRegisteredException;
37  import at.ac.tuwien.infosys.sm4all.copal.api.util.NotRegisteredException;
38  import at.ac.tuwien.infosys.sm4all.copal.api.util.Registry;
39  
40  /**
41   * This class is used with the {@link ContextListener}s to create queries which
42   * listen on occurrence of processed events (i.e. events for which all
43   * {@link ContextEventAction}s are finished}.
44   * 
45   * @author sanjin
46   */
47  public class ProcessedEventQuery extends ContextQuery implements
48          Registry<String, ContextListener> {
49  
50      private static final Logger LOGGER = Logger.getLogger(ProcessedEventQuery.class);
51  
52      private final Map<String, ContextListener> listeners = new HashMap<String, ContextListener>();
53      private final ReadWriteLock lock = new ReentrantReadWriteLock();
54      private final AtomicReference<ContextEvent> lastEvent = new AtomicReference<ContextEvent>();
55      private final Tracker<ContextSecurityManager> tracker = new Tracker<ContextSecurityManager>(
56              ContextSecurityManager.class.getName());
57  
58      /**
59       * Create instance of query for processed events. The <code>name</code>
60       * should be globally unique, meaning that if two context query have same
61       * <code>name</code> then their <code>listenedType</code>s and
62       * <code>criteria</code>s are equal. The <code>listenedType</code> is name
63       * of {@link ContextEventType} to be caught.
64       * 
65       * @param name the globally unique name of the query.
66       * @param listenedType the name of listened {@link ContextEventType}.
67       * @throws NullPointerException if specified name or event name is
68       *         <code>null</code>.
69       * @throws IllegalArgumentException if specified name or event name is an
70       *         empty or blank string.
71       */
72      public ProcessedEventQuery(final String name, final String listenedType) {
73          super(name, listenedType);
74      }
75  
76      /**
77       * Create instance of the query for processed events. The <code>name</code>
78       * should be globally unique, meaning that if two context queries have same
79       * <code>name</code> then their <code>listenedType</code>s and
80       * <code>criteria</code>s are equal. The <code>listenedType</code> is name
81       * of {@link ContextEventType} on which <code>criteria</code> is executed.
82       * The <code>criteria</code> is the logical statement which further
83       * separates the processed events of interest. If query should catch all
84       * <code>listenedType</code> events, we set <code>criteria</code> to be
85       * <code>null</code> or blank string or use
86       * {@link ContextQuery#ContextQuery(String, String) ContextQuery(String,
87       * String)} constructor.
88       * 
89       * @param name the globally unique name of the query.
90       * @param listenedType the name of listened {@link ContextEventType}.
91       * @param criteria the logical expression.
92       * @throws NullPointerException if specified name or event name is
93       *         <code>null</code>.
94       * @throws IllegalArgumentException if specified name or event name is an
95       *         empty or blank string.
96       */
97      public ProcessedEventQuery(final String name, final String listenedType,
98              final String criteria) {
99          super(name, listenedType, criteria);
100     }
101 
102     @Override
103     public final void start(final BundleContext bundleContext) {
104         this.tracker.start(bundleContext);
105     }
106 
107     @Override
108     public final void stop(final BundleContext bundleContext) {
109         this.tracker.stop(bundleContext);
110     }
111 
112     @Override
113     public void destroy() throws QueryDestroyedException {
114         super.destroy();
115 
116         unregisterAll();
117     }
118 
119     @Override
120     public void onEvent(final ContextEvent event)
121             throws QueryDestroyedException {
122         if (null == event) {
123             throw new NullPointerException("Event cannot be null.");
124         }
125 
126         this.lock.readLock().lock();
127         try {
128             if (isDestroyed()) {
129                 throw new QueryDestroyedException(this);
130             }
131 
132             if (event.isStale()) {
133                 if (LOGGER.isEnabledFor(Level.WARN)) {
134                     LOGGER.warn(MessageFormat.format(
135                             "Dropped stale event ''{0}'' from ''{1}''!",
136                             event.getType().getName(), event.getSourceID()));
137                 }
138             } else {
139                 for (final ContextListener listener : this.listeners.values()) {
140                     if (isAuthorized(listener, event)) {
141                         listener.onEvent(event);
142                     }
143                 }
144             }
145         } finally {
146             this.lastEvent.set(event);
147             this.lock.readLock().unlock();
148         }
149     }
150 
151     /**
152      * Register specified {@link ContextListener} with this
153      * {@link ProcessedEventQuery}. If {@link ContextEvent} is caught, for which
154      * the criteria evaluates to <code>true</code>, the specified
155      * {@link ContextListener} will be invoked.
156      * 
157      * @param listener the {@link ContextListener}.
158      * @throws AlreadyRegisteredException if a {@link ContextListener} with same
159      *         name is already registered.
160      * @throws NullPointerException if specified {@link ContextListener} is
161      *         <code>null</code>.
162      * @throws QueryDestroyedException if this {@link ProcessedEventQuery} has
163      *         been previously destroyed.
164      */
165     @Override
166     public void register(final ContextListener listener)
167             throws AlreadyRegisteredException, QueryDestroyedException {
168         if (null == listener) {
169             throw new NullPointerException("Listener cannot be null.");
170         }
171 
172         final String listenerName = listener.getName();
173 
174         this.lock.writeLock().lock();
175         try {
176             if (isDestroyed()) {
177                 throw new QueryDestroyedException(this);
178             }
179 
180             if (this.listeners.containsKey(listenerName)) {
181                 throw new AlreadyRegisteredException(listenerName);
182             }
183 
184             this.listeners.put(listenerName, listener);
185             final ContextEvent event = this.lastEvent.get();
186             if ((null != event) && !event.isStale()
187                     && isAuthorized(listener, event)) {
188                 listener.onEvent(event);
189             }
190 
191             if (LOGGER.isInfoEnabled()) {
192                 LOGGER.info(MessageFormat.format(
193                         "Successfully registered listener ''{0}'' with query ''{1}''.",
194                         listenerName, getName()));
195             }
196         } finally {
197             this.lock.writeLock().unlock();
198         }
199     }
200 
201     /**
202      * Unregister {@link ContextListener} with specified name from this
203      * {@link ProcessedEventQuery}. The {@link ContextListener} will not receive
204      * any further {@link ContextEvent}s from this {@link ProcessedEventQuery}.
205      * 
206      * @param listenerName the name of the {@link ContextListener}.
207      * @throws NotRegisteredException if a {@link ContextListener} with
208      *         specified name is not registered.
209      * @throws NullPointerException if specified name of the
210      *         {@link ContextListener} is <code>null</code>.
211      */
212     @Override
213     public void unregister(final String listenerName)
214             throws NotRegisteredException {
215         if (null == listenerName) {
216             throw new NullPointerException("Listener name cannot be null.");
217         }
218 
219         this.lock.writeLock().lock();
220         try {
221             if (!this.listeners.containsKey(listenerName)) {
222                 throw new NotRegisteredException(listenerName);
223             }
224 
225             if (!isDestroyed()) {
226                 this.listeners.remove(listenerName);
227                 if (LOGGER.isInfoEnabled()) {
228                     LOGGER.info(MessageFormat.format(
229                             "Successfully unregistered listener ''{0}'' from query ''{1}''.",
230                             listenerName, getName()));
231                 }
232             }
233         } finally {
234             this.lock.writeLock().unlock();
235         }
236     }
237 
238     /**
239      * Returns if a {@link ContextListener} with specified name is currently
240      * registered.
241      * 
242      * @param listenerName the name of the {@link ContextListener}.
243      * @return if a {@link ContextListener} with specified name is currently
244      *         registered.
245      * @throws NullPointerException if specified name of {@link ContextListener}
246      *         is <code>null</code>.
247      */
248     @Override
249     public boolean isRegistered(final String listenerName) {
250         if (null == listenerName) {
251             throw new NullPointerException("Listener name cannot be null.");
252         }
253 
254         final boolean result;
255 
256         this.lock.readLock().lock();
257         try {
258             result = this.listeners.containsKey(listenerName);
259         } finally {
260             this.lock.readLock().unlock();
261         }
262 
263         return result;
264     }
265 
266     /**
267      * Returns {@link ContextListener} with specified name or <code>null</code>
268      * if there is no such {@link ContextListener} registered.
269      * 
270      * @param listenerName the name of the {@link ContextListener}.
271      * @return the {@link ContextListener} or <code>null</code> if there is no
272      *         such {@link ContextListener} registered.
273      * @throws NullPointerException if specified name of {@link ContextListener}
274      *         is <code>null</code>.
275      */
276     @Override
277     public ContextListener get(final String listenerName) {
278         if (null == listenerName) {
279             throw new NullPointerException("Listener name cannot be null.");
280         }
281 
282         ContextListener result = null;
283 
284         this.lock.readLock().lock();
285         try {
286             if (this.listeners.containsKey(listenerName)) {
287                 result = this.listeners.get(listenerName);
288             }
289         } finally {
290             this.lock.readLock().unlock();
291         }
292 
293         return result;
294     }
295 
296     /**
297      * Returns all currently registered {@link ContextListener}s.
298      * 
299      * @return all currently registered {@link ContextListener}s.
300      */
301     @Override
302     public ContextListener[] getAll() {
303         final Collection<ContextListener> result;
304 
305         this.lock.readLock().lock();
306         try {
307             result = this.listeners.values();
308         } finally {
309             this.lock.readLock().unlock();
310         }
311 
312         return result.toArray(new ContextListener[result.size()]);
313     }
314 
315     private boolean isAuthorized(final ContextListener listener,
316             final ContextEvent event) {
317         final boolean result;
318 
319         if (event.hasAuthorizations()) {
320             final ContextSecurityManager securityManager = this.tracker.getService();
321             if (null == securityManager) {
322                 if (LOGGER.isEnabledFor(Level.ERROR)) {
323                     LOGGER.error(MessageFormat.format(
324                             "Cannot authorize listener ''{0}'' for event ''{1}''! No security manager.",
325                             listener.getName(), event.getType().getName()));
326                 }
327                 result = false;
328             } else {
329                 result = securityManager.authorized(listener, event);
330             }
331         } else {
332             result = true;
333         }
334 
335         return result;
336     }
337 
338     private void unregisterAll() {
339         if (LOGGER.isDebugEnabled()) {
340             LOGGER.debug(MessageFormat.format(
341                     "Unregistering all listeners from query ''{0}''.",
342                     getName()));
343         }
344 
345         this.lock.writeLock().lock();
346         try {
347             for (final String listenerName : this.listeners.keySet()) {
348                 try {
349                     unregister(listenerName);
350                 } catch (final NotRegisteredException ex) {
351                     if (LOGGER.isEnabledFor(Level.WARN)) {
352                         LOGGER.warn(
353                                 MessageFormat.format(
354                                         "Failed to unregister listener ''{0}'' from query ''{1}''! Ignoring.",
355                                         listenerName, getName()), ex);
356                     }
357                 }
358             }
359 
360             this.listeners.clear();
361         } finally {
362             this.lock.writeLock().unlock();
363         }
364 
365         if (LOGGER.isInfoEnabled()) {
366             LOGGER.info(MessageFormat.format(
367                     "All listeners from query ''{0}'' unregistered!", getName()));
368         }
369     }
370 }