001/*
002 * GWTEventService
003 * Copyright (c) 2011 and beyond, strawbill UG (haftungsbeschr?nkt)
004 *
005 * This is free software; you can redistribute it and/or modify it
006 * under the terms of the GNU Lesser General Public License as
007 * published by the Free Software Foundation; either version 3 of
008 * the License, or (at your option) any later version.
009 * Other licensing for GWTEventService may also be possible on request.
010 * Please view the license.txt of the project for more information.
011 *
012 * This software is distributed in the hope that it will be useful,
013 * but WITHOUT ANY WARRANTY; without even the implied warranty of
014 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
015 * Lesser General Public License for more details.
016 *
017 * You should have received a copy of the GNU Lesser General Public
018 * License along with this software; if not, write to the Free
019 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
020 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
021 */
022package de.novanic.eventservice.service.registry;
023
024import de.novanic.eventservice.client.event.filter.EventFilter;
025import de.novanic.eventservice.client.event.Event;
026import de.novanic.eventservice.client.event.DomainEvent;
027import de.novanic.eventservice.client.event.domain.Domain;
028import de.novanic.eventservice.client.event.domain.DomainFactory;
029import de.novanic.eventservice.config.EventServiceConfiguration;
030import de.novanic.eventservice.client.event.listener.unlisten.UnlistenEvent;
031import de.novanic.eventservice.client.event.listener.unlisten.UnlistenEventListener;
032import de.novanic.eventservice.logger.ServerLogger;
033import de.novanic.eventservice.logger.ServerLoggerFactory;
034import de.novanic.eventservice.service.EventServiceException;
035import de.novanic.eventservice.service.connection.strategy.connector.ConnectionStrategyServerConnector;
036import de.novanic.eventservice.service.registry.user.*;
037import de.novanic.eventservice.service.registry.domain.ListenDomainAccessor;
038import de.novanic.eventservice.service.UserTimeoutListener;
039import de.novanic.eventservice.event.listener.unlisten.UnlistenEventFilter;
040
041import java.util.*;
042
043/**
044 * The EventRegistry handles the users/clients and the events per domain. Users can be registered for a domain/context
045 * to receive events for the according domain.
046 * User specific events can be handled domain-less, when the user is registered.
047 * The EventRegistry is used by {@link de.novanic.eventservice.service.EventServiceImpl}.
048 *
049 * <br>The client id is required, because the connection to every client must be kept open.
050 *
051 * @see de.novanic.eventservice.service.EventServiceImpl
052 *
053 * @author sstrohschein
054 * <br>Date: 05.06.2008
055 * <br>Time: 19:12:35
056 */
057public class DefaultEventRegistry implements EventRegistry, ListenDomainAccessor
058{
059    private static final ServerLogger LOG = ServerLoggerFactory.getServerLogger(DefaultEventRegistry.class.getName());
060
061    private final EventServiceConfiguration myConfiguration;
062    private final DomainUserMapping myDomainUserMapping;
063    private final UserManager myUserManager;
064    private final UserActivityScheduler myUserActivityScheduler;
065
066    /**
067     * Creates a new EventRegistry with a configuration ({@link de.novanic.eventservice.config.EventServiceConfiguration}).
068     * The {@link EventRegistryFactory} should be used instead of calling that constructor directly.
069     * @param aConfiguration configuration
070     * @see de.novanic.eventservice.service.registry.EventRegistryFactory#getEventRegistry()
071     */
072    protected DefaultEventRegistry(EventServiceConfiguration aConfiguration) {
073        myConfiguration = aConfiguration;
074        myDomainUserMapping = new DomainUserMapping();
075        myUserManager = UserManagerFactory.getInstance().getUserManager(aConfiguration);
076        myUserActivityScheduler = myUserManager.getUserActivityScheduler();
077        myUserActivityScheduler.addTimeoutListener(new TimeoutListener());
078        myUserManager.activateUserActivityScheduler();
079
080        LOG.info("Configuration changed - " + aConfiguration.toString());
081    }
082
083    /**
084     * Checks if the user is registered for any domain.
085     * @param aUserId the user to check
086     * @return true if registered, false if not registered
087     */
088    public boolean isUserRegistered(String aUserId) {
089        UserInfo theUserInfo = getUserInfo(aUserId);
090        return isUserRegistered(theUserInfo);
091    }
092
093    /**
094     * Checks if the user is registered for any domain.
095     * @param aUserInfo the user to check
096     * @return true if registered, false if not registered
097     */
098    private boolean isUserRegistered(UserInfo aUserInfo) {
099        return aUserInfo != null && myUserManager.isUserContained(aUserInfo);
100    }
101
102    /**
103     * Checks if the user is registered for the corresponding domain.
104     * @param aDomain the domain to check
105     * @param aUserId the user to check
106     * @return true if registered, false if not registered
107     */
108    public boolean isUserRegistered(Domain aDomain, String aUserId) {
109        UserInfo theUserInfo = getUserInfo(aUserId);
110        return isUserRegistered(aDomain, theUserInfo);
111    }
112
113    /**
114     * Checks if the user is registered for the corresponding domain.
115     * @param aDomain the domain to check
116     * @param aUserInfo the user to check
117     * @return true if registered, false if not registered
118     */
119    private boolean isUserRegistered(Domain aDomain, UserInfo aUserInfo) {
120        return aDomain != null && aUserInfo != null && myDomainUserMapping.isUserContained(aDomain, aUserInfo);
121    }
122
123    /**
124     * Registers a user for listening for the corresponding domain. From now all events for the domain are recognized and
125     * will be returned when listen ({@link DefaultEventRegistry#listen(de.novanic.eventservice.service.connection.strategy.connector.ConnectionStrategyServerConnector , String)}) is called. The {@link de.novanic.eventservice.client.event.filter.EventFilter}
126     * is optional and can be NULL.
127     * @param aDomain the domain to listen
128     * @param aUserId the user to register
129     * @param anEventFilter EventFilter to filter the domain events (optional, can be NULL)
130     */
131    public void registerUser(final Domain aDomain, final String aUserId, EventFilter anEventFilter) {
132        //create UserInfo
133        UserInfo theUserInfo = myUserManager.addUser(aUserId);
134
135        //register UserInfo for the Domain
136        if(aDomain != null) {
137            myDomainUserMapping.addUser(aDomain, theUserInfo);
138
139            LOG.debug("User \"" + aUserId + "\" registered for domain \"" + aDomain + "\".");
140
141            //set EventFilter
142            setEventFilter(aDomain, theUserInfo, anEventFilter);
143        } else {
144            LOG.debug("User \"" + aUserId + "\" registered.");
145        }
146    }
147
148    /**
149     * The {@link de.novanic.eventservice.client.event.filter.EventFilter} for a user domain combination can be set or
150     * changed with that method. The {@link de.novanic.eventservice.client.event.filter.EventFilter} can be removed
151     * with the method {@link de.novanic.eventservice.service.registry.EventRegistry#removeEventFilter(de.novanic.eventservice.client.event.domain.Domain, String)}
152     * or when that method is called with NULL as the {@link de.novanic.eventservice.client.event.filter.EventFilter}
153     * parameter value.
154     * @param aDomain domain
155     * @param aUserId user
156     * @param anEventFilter new EventFilter
157     */
158    public void setEventFilter(final Domain aDomain, final String aUserId, EventFilter anEventFilter) {
159        UserInfo theUserInfo = getUserInfo(aUserId);
160        setEventFilter(aDomain, theUserInfo, anEventFilter);
161    }
162
163    /**
164     * The {@link de.novanic.eventservice.client.event.filter.EventFilter} for a user domain combination can be set or
165     * changed with that method. The {@link de.novanic.eventservice.client.event.filter.EventFilter} can be removed
166     * with the method {@link de.novanic.eventservice.service.registry.EventRegistry#removeEventFilter(de.novanic.eventservice.client.event.domain.Domain, String)}
167     * or when that method is called with NULL as the {@link de.novanic.eventservice.client.event.filter.EventFilter}
168     * parameter value.
169     * @param aDomain domain
170     * @param aUserInfo user
171     * @param anEventFilter new {@link de.novanic.eventservice.client.event.filter.EventFilter}
172     */
173    private void setEventFilter(final Domain aDomain, final UserInfo aUserInfo, EventFilter anEventFilter) {
174        if(aUserInfo != null) {
175            if(anEventFilter != null) {
176                LOG.debug(aUserInfo.getUserId() + ": EventFilter changed for domain \"" + aDomain + "\".");
177                aUserInfo.setEventFilter(aDomain, anEventFilter);
178            } else {
179                if(aUserInfo.removeEventFilter(aDomain)) {
180                    LOG.debug(aUserInfo.getUserId() + ": EventFilter removed from domain \"" + aDomain + "\".");
181                }
182            }
183        }
184    }
185
186    /**
187     * Returns the EventFilter for the user domain combination.
188     * @param aDomain domain
189     * @param aUserId user
190     * @return EventFilter for the domain
191     */
192    public EventFilter getEventFilter(Domain aDomain, String aUserId) {
193        UserInfo theUserInfo = getUserInfo(aUserId);
194        if(theUserInfo != null) {
195            return theUserInfo.getEventFilter(aDomain);
196        }
197        return null;
198    }
199
200    /**
201     * EventFilters can be removed for a user domain combination with that method.
202     * @param aDomain domain
203     * @param aUserId user
204     */
205    public void removeEventFilter(final Domain aDomain, final String aUserId) {
206        setEventFilter(aDomain, aUserId, null);
207    }
208
209    /**
210     * The listen method returns all events for the user (events for all domains where the user is registered and user
211     * specific events). If no events are available, the method waits a defined time before the events are returned.
212     * The listen method is designed for the EventService functionality. The client side calls the method with a defined
213     * interval to receive all events. If the client don't call the method in the interval, the user will be removed
214     * from the EventRegistry. The timeout time and the min. and max. waiting time can be configured by
215     * @param aServerEventListener {@link de.novanic.eventservice.service.connection.strategy.connector.ConnectionStrategyServerConnector} for the listening method
216     * @param aUserId user
217     * @return list of events
218     */
219    public List<DomainEvent> listen(ConnectionStrategyServerConnector aServerEventListener, String aUserId) {
220        UserInfo theUserInfo = getUserInfo(aUserId);
221        LOG.debug(aUserId + ": listen (UserInfo " + theUserInfo + ").");
222        if(theUserInfo != null) {
223            myUserActivityScheduler.reportUserActivity(theUserInfo);
224            try {
225                return aServerEventListener.listen(theUserInfo);
226            } catch(EventServiceException e) {
227                LOG.error("Error on listening for user \"" + theUserInfo + "\" with \"" + aServerEventListener.getClass().getName() + "\"!", e);
228            } finally {
229                myUserActivityScheduler.reportUserActivity(theUserInfo);
230            }
231        }
232        return null;
233    }
234
235    /**
236     * This method causes a stop of listening for a domain ({@link DefaultEventRegistry#listen(de.novanic.eventservice.service.connection.strategy.connector.ConnectionStrategyServerConnector , String)}).
237     * @param aDomain domain to stop listening
238     * @param aUserId user
239     */
240    public void unlisten(Domain aDomain, String aUserId) {
241        UserInfo theUserInfo = getUserInfo(aUserId);
242        if(theUserInfo != null) {
243            if(aDomain != null) {
244                LOG.debug(aUserId + ": unlisten (domain \"" + aDomain + "\").");
245                if(isUserRegistered(aDomain, theUserInfo)) {
246                    Set<Domain> theDomains = new HashSet<Domain>(1);
247                    theDomains.add(aDomain);
248                    addEvent(DomainFactory.UNLISTEN_DOMAIN, produceUnlistenEvent(theUserInfo, theDomains, false));
249                }
250                removeUser(aDomain, theUserInfo);
251            } else {
252                //An unlisten for the NULL-domain should only have an effect when the user isn't still registered for a domain.
253                if(!myDomainUserMapping.isUserContained(theUserInfo)) {
254                    unlisten(theUserInfo, false);
255                }
256            }
257        }
258    }
259
260    /**
261     * This method causes a stop of listening for all domains ({@link DefaultEventRegistry#listen(de.novanic.eventservice.service.connection.strategy.connector.ConnectionStrategyServerConnector , String)}).
262     * @param aUserId user
263     */
264    public void unlisten(String aUserId) {
265        UserInfo theUserInfo = getUserInfo(aUserId);
266        unlisten(theUserInfo, false);
267    }
268
269    /**
270     * This method causes a stop of listening for all domains ({@link DefaultEventRegistry#listen(de.novanic.eventservice.service.connection.strategy.connector.ConnectionStrategyServerConnector , String)}).
271     * @param aUserInfo user
272     * @param isTimeout reason for the unlistening (timeout or leave of a specific domain)
273     */
274    private void unlisten(UserInfo aUserInfo, boolean isTimeout) {
275        if(aUserInfo != null) {
276            final String theUserId = aUserInfo.getUserId();
277            LOG.debug(theUserId + ": unlisten.");
278            Set<Domain> theDomains = myDomainUserMapping.getDomains(aUserInfo);
279            addEvent(DomainFactory.UNLISTEN_DOMAIN, produceUnlistenEvent(aUserInfo, theDomains, isTimeout));
280            removeUser(aUserInfo);
281        }
282    }
283
284    /**
285     * Removes the user from a domain.
286     * @param aDomain domain
287     * @param aUserInfo user
288     * @return true when the user is removed from the domain, otherwise false
289     */
290    private boolean removeUser(Domain aDomain, UserInfo aUserInfo) {
291        boolean isUserRemoved = myDomainUserMapping.removeUser(aDomain, aUserInfo);
292        if(isUserRemoved) {
293            LOG.debug("User \"" + aUserInfo + "\" removed from domain \"" + aDomain + "\".");
294        }
295
296        if(!myDomainUserMapping.isUserContained(aUserInfo)) {
297            myUserManager.removeUser(aUserInfo.getUserId());
298        } else {
299            //remove the EventFilter if the user isn't removed completely
300            aUserInfo.removeEventFilter(aDomain);
301        }
302
303        return isUserRemoved;
304    }
305
306    /**
307     * Removes a user from all domains.
308     * @param aUserInfo user
309     */
310    private void removeUser(UserInfo aUserInfo) {
311        myDomainUserMapping.removeUser(aUserInfo);
312        if(myUserManager.removeUser(aUserInfo.getUserId()) != null) {
313            LOG.debug("User \"" + aUserInfo + "\" removed.");
314        }
315    }
316
317    /**
318     * Returns all domains where the user is registered to.
319     * @param aUserId user id
320     * @return domains where the user is registered to
321     */
322    public Set<Domain> getListenDomains(String aUserId) {
323        UserInfo theUserInfo = getUserInfo(aUserId);
324        return getListenDomains(theUserInfo);
325    }
326
327    /**
328     * Returns all domains where the user is registered to.
329     * @param aUserInfo user
330     * @return domains where the user is registered to
331     */
332    private Set<Domain> getListenDomains(UserInfo aUserInfo) {
333        return myDomainUserMapping.getDomains(aUserInfo);
334    }
335
336    /**
337     * Returns all registered/activated domains.
338     * @return all registered/activated domains
339     */
340    public Set<Domain> getListenDomains() {
341        return myDomainUserMapping.getDomains();
342    }
343
344    /**
345     * Returns all registered users/clients.
346     * To get only the registered users/client of a specific {@link de.novanic.eventservice.client.event.domain.Domain},
347     * the method {@link de.novanic.eventservice.service.registry.EventRegistry#getRegisteredUserIds(de.novanic.eventservice.client.event.domain.Domain)}
348     * can be used instead.
349     * @return registered users/clients
350     */
351    public Set<String> getRegisteredUserIds() {
352        return getUserIds(myUserManager.getUsers());
353    }
354
355    /**
356     * Returns all registered users/client of a specific {@link de.novanic.eventservice.client.event.domain.Domain}.
357     * To get all the registered users/client (of all domains), the method {@link EventRegistry#getRegisteredUserIds()}
358     * can be used instead.
359     * @param aDomain domain
360     * @return registered users/client of the specific domain
361     */
362    public Set<String> getRegisteredUserIds(Domain aDomain) {
363        return getUserIds(myDomainUserMapping.getUsers(aDomain));
364    }
365
366    /**
367     * Adds an event to a domain.
368     * @param aDomain domain for the event
369     * @param anEvent event to add
370     */
371    public void addEvent(Domain aDomain, Event anEvent) {
372        LOG.debug("Event \"" + anEvent + "\" added to domain \"" + aDomain + "\".");
373        final Set<UserInfo> theDomainUsers = myDomainUserMapping.getUsers(aDomain);
374        //if the domain doesn't exist/no users assigned, no users must be notified for the event...
375        if(theDomainUsers != null) {
376            for(UserInfo theUserInfo: theDomainUsers) {
377                addEvent(aDomain, theUserInfo, anEvent);
378            }
379        }
380    }
381
382    /**
383     * Adds an event directly to a user. The user must be registered to any domain.
384     * @param aUserId user
385     * @param anEvent event
386     */
387    public void addEventUserSpecific(String aUserId, Event anEvent) {
388        UserInfo theUserInfo = getUserInfo(aUserId);
389        addEventUserSpecific(theUserInfo, anEvent);
390    }
391
392    /**
393     * Adds an event directly to a user. The user must be registered to any domain.
394     * @param aUserInfo user
395     * @param anEvent event
396     */
397    private void addEventUserSpecific(UserInfo aUserInfo, Event anEvent) {
398        if(aUserInfo != null) {
399            LOG.debug("User specific event \"" + anEvent + "\" added to client id \"" + aUserInfo + "\".");
400            addEvent(DomainFactory.USER_SPECIFIC_DOMAIN, aUserInfo, anEvent);
401        }
402    }
403
404    /**
405     * Registers an {@link de.novanic.eventservice.client.event.listener.unlisten.UnlistenEvent} which is triggered on a
406     * timeout or when a user/client leaves a {@link de.novanic.eventservice.client.event.domain.Domain}. An
407     * {@link de.novanic.eventservice.client.event.listener.unlisten.UnlistenEvent} is hold at the server side and can
408     * contain custom data. Other users/clients can use the custom data when the event is for example triggered by a timeout.
409     * @param aUserId user to register the {@link de.novanic.eventservice.client.event.listener.unlisten.UnlistenEvent} to
410     * @param anUnlistenScope scope of the unlisten events to receive
411     * @param anUnlistenEvent {@link de.novanic.eventservice.client.event.listener.unlisten.UnlistenEvent} which should
412     * be transferred to other users/clients when a timeout occurs or a domain is leaved.
413     */
414    public void registerUnlistenEvent(String aUserId, UnlistenEventListener.Scope anUnlistenScope, UnlistenEvent anUnlistenEvent) {
415        registerUser(DomainFactory.UNLISTEN_DOMAIN, aUserId, new UnlistenEventFilter(this, aUserId, anUnlistenScope));
416        UserInfo theUserInfo = getUserInfo(aUserId);
417        theUserInfo.setUnlistenEvent(anUnlistenEvent);
418    }
419
420    /**
421     * Returns the initialized {@link de.novanic.eventservice.config.EventServiceConfiguration}
422     * @return configuration {@link de.novanic.eventservice.config.EventServiceConfiguration}
423     */
424    public EventServiceConfiguration getConfiguration() {
425        return myConfiguration;
426    }
427
428    /**
429     * Adds an event to a user in a domain.
430     * @param aDomain domain for the event
431     * @param aUserInfo user
432     * @param anEvent event to add
433     */
434    private void addEvent(Domain aDomain, UserInfo aUserInfo, Event anEvent) {
435        if(isEventValid(anEvent, aUserInfo.getEventFilter(aDomain))) {
436            aUserInfo.addEvent(aDomain, anEvent);
437            LOG.debug(anEvent + " for user \"" + aUserInfo + "\".");
438        }
439    }
440
441    /**
442     * Checks if the EventFilter recognizes the event as valid. When no EventFilter is available (NULL),
443     * the event is always valid.
444     * @param anEvent event
445     * @param anEventFilter EventFilter to check the event
446     * @return true when the event is valid, false when the event isn't valid (filtered by the EventFilter)
447     */
448    private boolean isEventValid(Event anEvent, EventFilter anEventFilter) {
449        return (anEventFilter == null || !(anEventFilter.match(anEvent)));
450    }
451
452    /**
453     * Determines the UserInfo with the user id.
454     * @param aUserId user
455     * @return UserInfo according to the user id
456     */
457    private UserInfo getUserInfo(final String aUserId) {
458        return myUserManager.getUser(aUserId);
459    }
460
461    /**
462     * Initializes an {@link de.novanic.eventservice.client.event.listener.unlisten.UnlistenEvent} with required information
463     * like the unlistened domain, user id of the unlistened user/client and the reason for the {@link de.novanic.eventservice.client.event.listener.unlisten.UnlistenEvent}.
464     * @param aUserInfo user
465     * @param aDomains unlistened domains
466     * @param isTimeout true when the unlisten event is caused by a timeout, false when the unlisten event is caused by a regular unlisten call
467     * @return produced {@link de.novanic.eventservice.client.event.listener.unlisten.UnlistenEvent}
468     */
469    private UnlistenEvent produceUnlistenEvent(UserInfo aUserInfo, Set<Domain> aDomains, boolean isTimeout) {
470        final UnlistenEvent theUnlistenEvent = aUserInfo.getUnlistenEvent();
471        theUnlistenEvent.setUserId(aUserInfo.getUserId());
472        theUnlistenEvent.setDomains(aDomains);
473        theUnlistenEvent.setTimeout(isTimeout);
474        return theUnlistenEvent;
475    }
476
477    /**
478     * Generates a set of user ids on the basis of {@link de.novanic.eventservice.service.registry.user.UserInfo} objects
479     * @param aUserInfoSet set of users ({@link de.novanic.eventservice.service.registry.user.UserInfo})
480     * @return set of user ids
481     */
482    private Set<String> getUserIds(Collection<UserInfo> aUserInfoSet) {
483        if(aUserInfoSet == null) {
484            return new HashSet<String>(0);
485        }
486        
487        Set<String> theUserIdSet = new HashSet<String>(aUserInfoSet.size());
488        for(UserInfo theUserInfo: aUserInfoSet) {
489            theUserIdSet.add(theUserInfo.getUserId());
490        }
491        return theUserIdSet;
492    }
493
494    /**
495     * TimeoutListener to clean up inactive users/clients. The timeout is checked with
496     * {@link de.novanic.eventservice.service.registry.user.UserActivityScheduler}.
497     */
498    private class TimeoutListener implements UserTimeoutListener
499    {
500        /**
501         * The method onTimeout is called when a timeout is recognized for the user.
502         * It causes a unlisten call ({@link de.novanic.eventservice.service.registry.DefaultEventRegistry#unlisten(String)})
503         * to clean up the inactive user/client.
504         * @param aUserInfo the inactive user
505         */
506        public void onTimeout(UserInfo aUserInfo) {
507            LOG.debug(aUserInfo.getUserId() + ": timeout.");
508            unlisten(aUserInfo, true);
509        }
510    }
511}