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.client.event;
023
024import com.google.gwt.user.client.rpc.AsyncCallback;
025
026import java.util.List;
027
028import com.google.gwt.user.client.rpc.StatusCodeException;
029import de.novanic.eventservice.client.config.ConfigurationTransferableDependentFactory;
030import de.novanic.eventservice.client.config.EventServiceConfigurationTransferable;
031import de.novanic.eventservice.client.connection.callback.AsyncCallbackWrapper;
032import de.novanic.eventservice.client.connection.strategy.connector.ConnectionStrategyClientConnector;
033import de.novanic.eventservice.client.event.listener.EventNotification;
034import de.novanic.eventservice.client.connection.strategy.connector.RemoteEventConnector;
035import de.novanic.eventservice.client.logger.ClientLogger;
036import de.novanic.eventservice.client.logger.ClientLoggerFactory;
037import de.novanic.eventservice.client.event.domain.Domain;
038import de.novanic.eventservice.client.event.domain.DomainFactory;
039import de.novanic.eventservice.client.event.filter.EventFilter;
040import de.novanic.eventservice.client.event.listener.unlisten.UnlistenEvent;
041import de.novanic.eventservice.client.event.listener.unlisten.DefaultUnlistenEvent;
042import de.novanic.eventservice.client.event.listener.unlisten.UnlistenEventListener;
043
044/**
045 * RemoteEventConnector should handle the connections between client- and the server side.
046 *
047 * @author sstrohschein
048 *         <br>Date: 14.10.2008
049 *         <br>Time: 11:34:45
050 */
051public abstract class DefaultRemoteEventConnector implements RemoteEventConnector
052{
053    private static final ClientLogger LOG = ClientLoggerFactory.getClientLogger();
054
055    private boolean isActive;
056    private EventServiceConfigurationTransferable myConfiguration;
057    private ConnectionStrategyClientConnector myConnectionStrategyClientConnector;
058    private UnlistenEvent myUnlistenEvent;
059    private int myErrorCount;
060
061    /**
062     * Initializes the listen method implementation with a {@link de.novanic.eventservice.client.connection.strategy.connector.ConnectionStrategyClientConnector} from the configuration.
063     * That is required to specify the listen / connection strategy. The connection strategy can't be changed, when the listening has already started / an listener was added.
064     * @param aConfiguration configuration
065     */
066    public synchronized ConnectionStrategyClientConnector initListen(EventServiceConfigurationTransferable aConfiguration) {
067        if(isActive) {
068            throw new RemoteEventServiceRuntimeException("Invalid attempt to change the connection strategy after listening was started!");
069        }
070        if(aConfiguration == null) {
071            throw new RemoteEventServiceRuntimeException("Invalid attempt to initialize the listening without a configuration!");
072        }
073        myConfiguration = aConfiguration;
074        ConfigurationTransferableDependentFactory theConfigDependentFactory = ConfigurationTransferableDependentFactory.getInstance(aConfiguration);
075        return (myConnectionStrategyClientConnector = theConfigDependentFactory.getConnectionStrategyClientConnector());
076    }
077
078    /**
079     * Deactivates the connector for all domains (no events can be got from the domains).
080     */
081    public synchronized void deactivate() {
082        if(isActive) {
083            isActive = false;
084            myConnectionStrategyClientConnector.deactivate();
085            LOG.log("RemoteEventConnector deactivated.");
086        }
087    }
088
089    /**
090     * Checks if the connector is active (listening).
091     * @return true when active/listening, otherwise false
092     */
093    public boolean isActive() {
094        return isActive;
095    }
096
097    /**
098     * Activates the connector for the domain. An {@link de.novanic.eventservice.client.event.filter.EventFilter}
099     * to filter events on the server side is optional.
100     * @param aDomain domain to activate
101     * @param anEventFilter EventFilter to filter the events on the server side (optional)
102     * @param anEventNotification supports the notification about incoming events
103     * @param aCallback callback
104     */
105    public void activate(Domain aDomain, EventFilter anEventFilter, EventNotification anEventNotification, AsyncCallback<Void> aCallback) {
106        LOG.log("Activate RemoteEventConnector for domain \"" + aDomain + "\".");
107        activateStart(aDomain, anEventFilter, new ActivationCallback<Void>(anEventNotification, aCallback));
108    }
109
110    /**
111     * Registers an {@link de.novanic.eventservice.client.event.listener.unlisten.UnlistenEvent} to the server side which
112     * will be triggered  when a timeout or unlisten/deactivation for a domain occurs.
113     * The UnlistenEvent will also be hold at the client side to trigger on local timeouts (for e.g. connection errors).
114     * @param anUnlistenScope scope of the unlisten events to receive
115     * @param anUnlistenEvent {@link de.novanic.eventservice.client.event.listener.unlisten.UnlistenEvent} which can contain custom data
116     * @param aCallback callback
117     */
118    public void registerUnlistenEvent(UnlistenEventListener.Scope anUnlistenScope, UnlistenEvent anUnlistenEvent, AsyncCallback<Void> aCallback) {
119        myUnlistenEvent = anUnlistenEvent;
120    }
121
122    /**
123     * Activates the connector for the domain. An {@link de.novanic.eventservice.client.event.filter.EventFilter}
124     * to filter events on the server side is optional.
125     * @param aDomain domain to activate
126     * @param anEventFilter EventFilter to filter the events on the server side (optional)
127     * @param aCallback callback
128     */
129    protected abstract void activateStart(Domain aDomain, EventFilter anEventFilter, AsyncCallback<Void> aCallback);
130
131    /**
132     * Creates the {@link de.novanic.eventservice.client.event.listener.unlisten.UnlistenEvent} for local timeouts.
133     * @param anEventNotification {@link de.novanic.eventservice.client.event.listener.EventNotification} for the triggered
134     * {@link de.novanic.eventservice.client.event.listener.unlisten.UnlistenEvent}
135     */
136    private void fireUnlistenEvent(EventNotification anEventNotification) {
137        if(myUnlistenEvent == null) {
138            myUnlistenEvent = new DefaultUnlistenEvent();
139        }
140        myUnlistenEvent.setTimeout(false);
141        myUnlistenEvent.setLocal(true);
142        final DomainEvent theUnlistenDomainEvent = new DefaultDomainEvent(myUnlistenEvent, DomainFactory.UNLISTEN_DOMAIN);
143        anEventNotification.onNotify(theUnlistenDomainEvent);
144    }
145
146    /**
147     * Callback to activate listening of RemoteEventConnector.
148     */
149    private final class ActivationCallback<T> extends AsyncCallbackWrapper<T>
150    {
151        private final EventNotification myEventNotification;
152
153        private ActivationCallback(EventNotification anEventNotification, AsyncCallback<T> aCallback) {
154            super(aCallback);
155            myEventNotification = anEventNotification;
156        }
157
158        /**
159         * Starts listening to get events from the server side.
160         * @param aResult unused
161         * @see de.novanic.eventservice.client.event.GWTRemoteEventConnector.ListenEventCallback#callListen()
162         */
163        public void onSuccess(T aResult) {
164            if(!isActive) {
165                if(myConnectionStrategyClientConnector != null) {
166                    LOG.log("RemoteEventConnector activated.");
167                    isActive = true;
168                    final ListenEventCallback theListenEventCallback = new ListenEventCallback(myEventNotification);
169                    theListenEventCallback.callListen();
170                } else {
171                    throw new RemoteEventServiceRuntimeException("No connection strategy was set at the start of listening!");
172                }
173            }
174            super.onSuccess(aResult);
175        }
176
177        public void onFailure(Throwable aThrowable) {
178            LOG.error("Error on register client for domain!", aThrowable);
179            fireUnlistenEvent(myEventNotification);
180            super.onFailure(aThrowable);
181        }
182    }
183
184    /**
185     * The ListenEventCallback is used to produce the listen cycle. It is attached as callback for the listen server call.
186     */
187    private final class ListenEventCallback implements AsyncCallback<List<DomainEvent>>
188    {
189        private EventNotification myEventNotification;
190
191        private ListenEventCallback(EventNotification anEventNotification) {
192            myEventNotification = anEventNotification;
193        }
194
195        /**
196         * When an error occurs while listening for events, reconnect attempts are started (when configured) and an unlisten event
197         * will be processed when the connection error could not be solved (to clean-up the client side).
198         * @param aThrowable
199         */
200        public void onFailure(Throwable aThrowable) {
201            if((aThrowable instanceof StatusCodeException) && !isNotableStatusCode((StatusCodeException)aThrowable)) {
202                //Status code 0 is not handled as an error. Some browsers send this status code when the user leaves the site/application.
203                //The module is unloaded in this case and it has no negative effects to the application. Therefore it isn't a notable error.
204                LOG.log("The current connection was terminated with status code " + ((StatusCodeException)aThrowable).getStatusCode() + '.');
205                fireUnlistenEvent(myEventNotification); //client side clean-up
206            } else {
207                LOG.error("Error on processing event!", aThrowable);
208                if(++myErrorCount > myConfiguration.getReconnectAttemptCount()) {
209                    fireUnlistenEvent(myEventNotification); //client side clean-up
210                } else {
211                    LOG.log("Reconnecting after error...");
212                    callListen();
213                }
214            }
215        }
216
217        /**
218         * Calls listen on the server side and put itself as the callback to produces the listen cycle as long as the
219         * RemoteEventConnector is active.
220         * @param anEvents) events to process
221         * @see de.novanic.eventservice.client.event.GWTRemoteEventConnector.ListenEventCallback#callListen()
222         */
223        public void onSuccess(List<DomainEvent> anEvents) {
224            myErrorCount = 0;
225            if(anEvents != null) {
226                for(DomainEvent theEvent: anEvents) {
227                    myEventNotification.onNotify(theEvent);
228                }
229                callListen();
230            } else {
231                //if the remote service doesn't know the client, all listeners will be removed and the connection becomes inactive
232                deactivate();
233                myEventNotification.onAbort();
234            }
235        }
236
237        /**
238         * Calls listen at the server side to receive events.
239         */
240        public synchronized void callListen() {
241            if(isActive) {
242                //after getting an event, register itself to listen for the next events
243                myConnectionStrategyClientConnector.listen(myEventNotification, this);
244            }
245        }
246
247        /**
248         * Checks if the status code is a valid error code. For example status code 0 is rather an informational status code
249         * instead of a notable error status code.
250         * @param aStatusCodeException {@link StatusCodeException} which occurred with the status code
251         * @return true when it is a notable error status code, otherwise false
252         */
253        private boolean isNotableStatusCode(StatusCodeException aStatusCodeException) {
254            return aStatusCodeException.getStatusCode() != 0;
255        }
256    }
257}