001/**
002 *   GRANITE DATA SERVICES
003 *   Copyright (C) 2006-2013 GRANITE DATA SERVICES S.A.S.
004 *
005 *   This file is part of the Granite Data Services Platform.
006 *
007 *                               ***
008 *
009 *   Community License: GPL 3.0
010 *
011 *   This file is free software: you can redistribute it and/or modify
012 *   it under the terms of the GNU General Public License as published
013 *   by the Free Software Foundation, either version 3 of the License,
014 *   or (at your option) any later version.
015 *
016 *   This file is distributed in the hope that it will be useful, but
017 *   WITHOUT ANY WARRANTY; without even the implied warranty of
018 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
019 *   GNU General Public License for more details.
020 *
021 *   You should have received a copy of the GNU General Public License
022 *   along with this program. If not, see <http://www.gnu.org/licenses/>.
023 *
024 *                               ***
025 *
026 *   Available Commercial License: GraniteDS SLA 1.0
027 *
028 *   This is the appropriate option if you are creating proprietary
029 *   applications and you are not prepared to distribute and share the
030 *   source code of your application under the GPL v3 license.
031 *
032 *   Please visit http://www.granitedataservices.com/license for more
033 *   details.
034 */
035package org.granite.client.tide.server;
036
037import java.lang.reflect.Constructor;
038import java.lang.reflect.Type;
039import java.nio.charset.Charset;
040import java.util.ArrayList;
041import java.util.Date;
042import java.util.HashMap;
043import java.util.HashSet;
044import java.util.List;
045import java.util.Map;
046import java.util.Observable;
047import java.util.Observer;
048import java.util.Set;
049import java.util.Timer;
050import java.util.TimerTask;
051import java.util.concurrent.Executors;
052import java.util.concurrent.ScheduledExecutorService;
053import java.util.concurrent.ScheduledFuture;
054import java.util.concurrent.TimeUnit;
055
056import javax.annotation.PostConstruct;
057import javax.annotation.PreDestroy;
058import javax.inject.Named;
059
060import org.granite.client.configuration.Configuration;
061import org.granite.client.messaging.ClientAliasRegistry;
062import org.granite.client.messaging.Consumer;
063import org.granite.client.messaging.Producer;
064import org.granite.client.messaging.RemoteService;
065import org.granite.client.messaging.ResultFaultIssuesResponseListener;
066import org.granite.client.messaging.ServerApp;
067import org.granite.client.messaging.TopicAgent;
068import org.granite.client.messaging.channel.*;
069import org.granite.client.messaging.codec.MessagingCodec.ClientType;
070import org.granite.client.messaging.events.Event;
071import org.granite.client.messaging.events.FaultEvent;
072import org.granite.client.messaging.events.IncomingMessageEvent;
073import org.granite.client.messaging.events.IssueEvent;
074import org.granite.client.messaging.events.ResultEvent;
075import org.granite.client.messaging.messages.responses.FaultMessage;
076import org.granite.client.messaging.messages.responses.FaultMessage.Code;
077import org.granite.client.messaging.messages.responses.ResultMessage;
078import org.granite.client.messaging.transport.Transport;
079import org.granite.client.messaging.transport.TransportException;
080import org.granite.client.messaging.transport.TransportStatusHandler;
081import org.granite.client.platform.Platform;
082import org.granite.client.tide.ApplicationConfigurable;
083import org.granite.client.tide.Context;
084import org.granite.client.tide.ContextAware;
085import org.granite.client.tide.Identity;
086import org.granite.client.tide.impl.FaultHandler;
087import org.granite.client.tide.impl.ResultHandler;
088import org.granite.client.validation.InvalidValue;
089import org.granite.config.GraniteConfig;
090import org.granite.logging.Logger;
091import org.granite.util.ContentType;
092
093
094/**
095 * ServerSession provides an API to manage all communications with the server
096 * It can be setup as a managed bean with Spring or CDI or created manually and attached to a Tide context
097 *
098 * <pre>
099 * {@code
100 * ServerSession serverSession = tideContext.set(new ServerSession("/myapp", "localhost", 8080));
101 * }
102 * </pre>
103 *
104 * @author William DRAI
105 */
106@ApplicationConfigurable
107@Named
108public class ServerSession implements ContextAware {
109
110        private static Logger log = Logger.getLogger(ServerSession.class);
111        
112        public static final String SERVER_TIME_TAG = "org.granite.time";
113        public static final String SESSION_ID_TAG = "org.granite.sessionId";
114        public static final String SESSION_EXP_TAG = "org.granite.sessionExp";
115
116    public static final String CONTEXT_RESULT = "org.granite.tide.result";
117    public static final String CONTEXT_FAULT = "org.granite.tide.fault";
118    
119        public static final String LOGIN = "org.granite.client.tide.login";
120        public static final String LOGOUT = "org.granite.client.tide.logout";
121        public static final String SESSION_EXPIRED = "org.granite.client.tide.sessionExpired";
122
123
124    private ServerApp serverApp;
125        private ContentType contentType = ContentType.JMF_AMF;
126        private Class<? extends ChannelFactory> channelFactoryClass = null;
127    private Transport remotingTransport = null;
128    private Transport messagingTransport = null;
129    private Map<String, Transport> messagingTransports = new HashMap<String, Transport>();
130    
131        private Context context = null;
132
133        private Status status = new DefaultStatus();
134        
135        private String sessionId = null;
136
137        private LogoutState logoutState = new LogoutState();
138                
139        private String destination = "server";
140        private Object platformContext = null;
141    private ChannelBuilder defaultChannelBuilder = null;
142    private String defaultChannelType = null;
143    private ChannelFactory channelFactory;
144    private RemotingChannel remotingChannel = null;
145    private Map<String, MessagingChannel> messagingChannelsByType = new HashMap<String, MessagingChannel>();
146        protected Map<String, RemoteService> remoteServices = new HashMap<String, RemoteService>();
147        protected Map<String, TopicAgent> topicAgents = new HashMap<String, TopicAgent>();
148        private Set<String> packageNames = new HashSet<String>();
149        
150        
151    public ServerSession() throws Exception {
152        // Used for testing
153    }
154
155    /**
156     * Create a server session for the specified context root and server
157     * @param contextRoot context root
158     * @param serverName server host name
159     * @param serverPort server port
160     * @throws Exception
161     */
162    public ServerSession(String contextRoot, String serverName, int serverPort) {
163        this(contextRoot, false, serverName, serverPort);
164    }
165
166    /**
167     * Create a server session for the specified context root and server
168     * @param contextRoot context root
169     * @param secure server app is secured (https/wss/...)
170     * @param serverName server host name
171     * @param serverPort server port
172     * @throws Exception
173     */
174    public ServerSession(String contextRoot, boolean secure, String serverName, int serverPort) {
175        this.serverApp = new ServerApp(contextRoot, secure, serverName, serverPort);
176    }
177
178    /**
179     * Create a server session for the specified server app
180     * @param serverApp server application definition
181     */
182    public ServerSession(ServerApp serverApp) {
183        this.serverApp = serverApp;
184    }
185
186    /**
187     * Serialization type (default is JMF)
188     * @return content type
189     */
190    public ContentType getContentType() {
191                return contentType;
192        }
193
194    /**
195     * Set the serialization type
196     * @param contentType serialization type
197     */
198        public void setContentType(ContentType contentType) {
199                if (contentType == null)
200                        throw new NullPointerException("contentType cannot be null");
201                this.contentType = contentType;
202        }
203
204    /**
205     * Set the default channel builder
206     * @param channelBuilder channel builder
207     */
208    public void setDefaultChannelBuilder(ChannelBuilder channelBuilder) {
209        this.defaultChannelBuilder = channelBuilder;
210        if (channelFactory != null)
211            channelFactory.setDefaultChannelBuilder(defaultChannelBuilder);
212    }
213
214    /**
215     * Set the default channel type for messaging
216     * @param channelType channel type
217     */
218    public void setDefaultChannelType(String channelType) {
219        this.defaultChannelType = channelType;
220        if (channelFactory != null)
221            channelFactory.setDefaultChannelType(defaultChannelType);
222    }
223
224    /**
225     * Set a custom channel factory class
226     * @param channelFactoryClass channel factory class
227     */
228        public void setChannelFactoryClass(Class<? extends ChannelFactory> channelFactoryClass) {
229            this.channelFactoryClass = channelFactoryClass;
230        }
231
232    /**
233     * Set the server app for this server session
234     * @param serverApp server application definition
235     */
236        public void setServerApp(ServerApp serverApp) {
237        this.serverApp = serverApp;
238    }
239
240    /**
241     * Set the Tide context for this server session
242     * (internal method, should be set by the context itself)
243     * @param context Tide context
244     * @see org.granite.client.tide.ContextAware
245     */
246        public void setContext(Context context) {
247                this.context = context;
248        }
249
250    /**
251     * Current Tide context
252     * @return Tide context
253     */
254        public Context getContext() {
255                return this.context;
256        }
257
258    /**
259     * Set the platform context for this server session
260     * @param platformContext
261     */
262        public void setPlatformContext(Object platformContext) {
263            this.platformContext = platformContext;
264        }
265
266    /**
267     * Set an implementation of the Status interface to be notified of server related information
268     * @param status status
269     */
270        public void setStatus(Status status) {
271                this.status = status;
272        }
273
274    /**
275     * Status implementation
276     * @return status
277     */
278        public Status getStatus() {
279                return status;
280        }
281
282    /**
283     * Set the remoting transport
284     * @param transport remoting transport
285     */
286        public void setRemotingTransport(Transport transport) {
287                this.remotingTransport = transport;
288        }
289
290    /**
291     * Set the default messaging transport
292     * @param transport messaging transport
293     */
294        public void setMessagingTransport(Transport transport) {
295                this.messagingTransport = transport;
296        }
297
298    /**
299     * Set the messaging transport for the specified channel type
300     * @param channelType channel type
301     */
302    public void setMessagingTransport(String channelType, Transport transport) {
303        this.messagingTransports.put(channelType, transport);
304    }
305
306    /**
307     * Add a package name to scan for remote aliases
308     * @param packageName package name
309     */
310        public void addRemoteAliasPackage(String packageName) {         
311                this.packageNames.add(packageName);
312        }
313
314    /**
315     * Reset all package names to scan for remote aliases
316     * @param packageNames package names
317     */
318        public void setRemoteAliasPackages(Set<String> packageNames) {
319                this.packageNames.clear();
320                this.packageNames.addAll(packageNames);
321        }
322        
323        private GraniteConfig graniteConfig = null;
324        
325        public Object convert(Object value, Type expectedType) {
326                if (contentType == ContentType.JMF_AMF || graniteConfig == null)
327                        return value;
328                return graniteConfig.getConverters().convert(value, expectedType);
329        }
330
331    /**
332     * Configure and start the server session
333     * @throws Exception
334     */
335        public void start() throws Exception {
336            if (channelFactory != null)    // Already started
337                return;
338            
339            ClientAliasRegistry aliasRegistry = new ClientAliasRegistry();
340            aliasRegistry.registerAlias(InvalidValue.class);
341
342            if (channelFactoryClass != null) {
343                Constructor<? extends ChannelFactory> constructor = null;
344                try {
345                    constructor = channelFactoryClass.getConstructor(Object.class, Configuration.class);
346                    Configuration configuration = Platform.getInstance().newConfiguration();
347                    configuration.setClientType(ClientType.JAVA);
348                    configuration.load();
349                    graniteConfig = configuration.getGraniteConfig();
350                    channelFactory = constructor.newInstance(platformContext, configuration);
351                }
352                catch (NoSuchMethodException e) {
353                    constructor = channelFactoryClass.getConstructor(Object.class);
354                channelFactory = constructor.newInstance(platformContext);
355                }               
356            }
357            else if (contentType == ContentType.JMF_AMF)
358                        channelFactory = new JMFChannelFactory(platformContext);
359                else {
360                        Configuration configuration = Platform.getInstance().newConfiguration();
361                        configuration.setClientType(ClientType.JAVA);
362                        configuration.load();
363                        graniteConfig = configuration.getGraniteConfig();
364                        channelFactory = new AMFChannelFactory(platformContext, configuration);
365                }
366        channelFactory.setAliasRegistry(aliasRegistry);
367                channelFactory.setScanPackageNames(packageNames);
368
369        if (defaultChannelType != null)
370            channelFactory.setDefaultChannelType(defaultChannelType);
371                
372                if (remotingTransport != null) {
373                        channelFactory.setRemotingTransport(remotingTransport);
374            remotingTransport.setStatusHandler(statusHandler);
375        }
376                if (messagingTransport != null) {
377                        channelFactory.setMessagingTransport(messagingTransport);
378            messagingTransport.setStatusHandler(statusHandler);
379        }
380        for (Map.Entry<String, Transport> me : messagingTransports.entrySet()) {
381            channelFactory.setMessagingTransport(me.getKey(), me.getValue());
382            me.getValue().setStatusHandler(statusHandler);
383        }
384        if (defaultChannelBuilder != null)
385            channelFactory.setDefaultChannelBuilder(defaultChannelBuilder);
386                
387                if (defaultTimeToLive >= 0)
388                    channelFactory.setDefaultTimeToLive(defaultTimeToLive);
389                
390                channelFactory.start();
391
392        remotingChannel = channelFactory.newRemotingChannel("graniteamf", serverApp, 1);
393
394                sessionExpirationTimer = Executors.newSingleThreadScheduledExecutor();
395        }
396
397    /**
398     * Stop the server session and cleanup resources
399     * @throws Exception
400     */
401        public void stop() throws Exception {
402                try {
403                        if (sessionExpirationFuture != null) {
404                                sessionExpirationFuture.cancel(false);
405                                sessionExpirationFuture = null;
406                        }
407                        if (sessionExpirationTimer != null) {
408                                sessionExpirationTimer.shutdownNow();
409                                sessionExpirationTimer = null;
410                        }
411                }
412                finally {
413                        if (channelFactory != null) {
414                                channelFactory.stop();
415                                channelFactory = null;
416                        }
417                    
418            remotingChannel = null;
419                        messagingChannelsByType.clear();
420                }
421        }
422
423    /**
424     * Internal SPI to define how remoting/messaging elements are created
425     */
426        public static interface ServiceFactory {
427
428        /**
429         * Create a remote service for the specified channel and destination
430         * @param remotingChannel channel
431         * @param destination destination name
432         * @return remote service
433         */
434                public RemoteService newRemoteService(RemotingChannel remotingChannel, String destination);
435
436        /**
437         * Create a producer
438         * @param messagingChannel channel
439         * @param destination destination name
440         * @param topic subtopic
441         * @return producer
442         */
443                public Producer newProducer(MessagingChannel messagingChannel, String destination, String topic);
444
445        /**
446         * Create a consumer
447         * @param messagingChannel channel
448         * @param destination destination name
449         * @param topic subtopic
450         * @return consumer
451         */
452                public Consumer newConsumer(MessagingChannel messagingChannel, String destination, String topic);
453        }
454        
455        private static class DefaultServiceFactory implements ServiceFactory {
456                
457                @Override
458                public RemoteService newRemoteService(RemotingChannel remotingChannel, String destination) {
459                        return new RemoteService(remotingChannel, destination);
460                }
461                
462                @Override
463                public Producer newProducer(MessagingChannel messagingChannel, String destination, String topic) {
464                        return new Producer(messagingChannel, destination, topic);
465                }
466                
467                @Override
468                public Consumer newConsumer(MessagingChannel messagingChannel, String destination, String topic) {
469                        return new Consumer(messagingChannel, destination, topic);
470                }
471        }
472        
473        private ServiceFactory serviceFactory = new DefaultServiceFactory();
474
475    /**
476     * Set a custom service factory to create producer/consumers and remote services
477     * @param serviceFactory service factory
478     */
479        public void setServiceFactory(ServiceFactory serviceFactory) {
480                this.serviceFactory = serviceFactory;
481        }
482
483    /**
484     * Returns remote service for the internal destination
485     * Should generally not be used except for very advanced use, use {@link Component} instead
486     * @return internal remote service
487     */
488        public RemoteService getRemoteService() {
489                return getRemoteService(destination);
490        }
491
492    /**
493     * Returns a remote service for the specified destination
494     * Should generally not be used except for very advanced use, use {@link Component} instead
495     * @param destination destination name
496     * @return remote service
497     */
498        public synchronized RemoteService getRemoteService(String destination) {
499                if (remotingChannel == null)
500                        throw new IllegalStateException("Channel not defined for server session");
501                
502                RemoteService remoteService = remoteServices.get(destination);
503                if (remoteService == null) {
504                        remoteService = serviceFactory.newRemoteService(remotingChannel, destination);
505                        remoteServices.put(destination, remoteService);
506                }
507                return remoteService;
508        }
509
510    /**
511     * Return a messaging channel for the specified type
512     * @param channelType channel type
513     * @return messaging channel
514     * @see org.granite.client.messaging.channel.ChannelType
515     */
516    public MessagingChannel getMessagingChannel(String channelType) {
517        MessagingChannel messagingChannel = messagingChannelsByType.get(channelType);
518        if (messagingChannel != null)
519            return messagingChannel;
520
521        messagingChannel = channelFactory.newMessagingChannel(channelType, channelType + "amf", serverApp);
522        return messagingChannel;
523    }
524
525    /**
526     * Build a consumer for the specified channel type and destination
527     * @param destination destination name
528     * @param topic subtopic
529     * @param channelType channel type
530     * @return consumer
531     */
532        public synchronized Consumer getConsumer(String destination, String topic, String channelType) {
533        if (channelType == null)
534            channelType = channelFactory.getDefaultChannelType();
535
536        MessagingChannel messagingChannel = getMessagingChannel(channelType);
537                if (messagingChannel == null)
538                        throw new IllegalStateException("Channel not defined in server session for type " + channelType + "");
539                
540                String key = "C:" + destination + '@' + topic;
541                TopicAgent consumer = topicAgents.get(key);
542                if (consumer == null) {
543                        consumer = serviceFactory.newConsumer(messagingChannel, destination, topic);
544                        topicAgents.put(key, consumer);
545                }
546                return consumer instanceof Consumer ? (Consumer)consumer : null;
547        }
548
549    /**
550     * Build a consumer for the default channel type and destination
551     * @param destination destination name
552     * @param topic subtopic
553     * @return consumer
554     */
555    public synchronized Consumer getConsumer(String destination, String topic) {
556        return getConsumer(destination, topic, channelFactory.getDefaultChannelType());
557    }
558
559    /**
560     * Build a producer for the specified channel type and destination
561     * @param destination destination name
562     * @param topic subtopic
563     * @param channelType channel type
564     * @return producer
565     */
566        public synchronized Producer getProducer(String destination, String topic, String channelType) {
567        if (channelType == null)
568            channelType = channelFactory.getDefaultChannelType();
569
570        MessagingChannel messagingChannel = getMessagingChannel(channelType);
571        if (messagingChannel == null)
572                        throw new IllegalStateException("Channel not defined for server session");
573                
574                String key = "P:" + destination + '@' + topic;
575                TopicAgent producer = topicAgents.get(key);
576                if (producer == null) {
577                        producer = serviceFactory.newProducer(messagingChannel, destination, topic);
578                        topicAgents.put(key, producer);
579                }
580                return producer instanceof Producer ? (Producer)producer : null;
581        }
582
583    /**
584     * Build a producer for the default channel type and destination
585     * @param destination destination name
586     * @param topic subtopic
587     * @return producer
588     */
589    public synchronized Producer getProducer(String destination, String topic) {
590        return getProducer(destination, topic, channelFactory.getDefaultChannelType());
591    }
592
593    /**
594     * Current remote session id
595     * @return session id
596     */
597        public String getSessionId() {
598                return sessionId;
599        }
600
601    /**
602     * Is logging out ?
603     * @return true if logout in progress
604     */
605        public boolean isLogoutInProgress() {
606                return logoutState.logoutInProgress;
607        }
608
609        private ScheduledExecutorService sessionExpirationTimer = null;
610        private ScheduledFuture<?> sessionExpirationFuture = null;
611        
612        private Runnable sessionExpirationTask = new Runnable() {
613                @Override
614                public void run() {
615                        Identity identity = context.byType(Identity.class);
616                        identity.checkLoggedIn(null);
617                }
618        };
619        
620        private void rescheduleSessionExpirationTask(long serverTime, int sessionExpirationDelay) {
621                Identity identity = context.byType(Identity.class);
622                if (identity == null || !identity.isLoggedIn()) // No session expiration tracking if user not logged in
623                        return;
624                
625                long clientOffset = serverTime - new Date().getTime();
626                sessionExpirationFuture = sessionExpirationTimer.schedule(sessionExpirationTask, clientOffset + sessionExpirationDelay*1000L + 1500L, TimeUnit.MILLISECONDS);
627        }
628
629    /**
630     * Callback called when a remoting response is received
631     * @param event event
632     */
633        public void onResultEvent(Event event) {
634                if (sessionExpirationFuture != null)
635                        sessionExpirationFuture.cancel(false);
636                
637                String oldSessionId = sessionId;
638                
639                if (event instanceof ResultEvent) {
640                        ResultMessage message = ((ResultEvent)event).getMessage();
641                        sessionId = (String)message.getHeader(SESSION_ID_TAG);
642                        if (sessionId != null) {
643                                long serverTime = (Long)message.getHeader(SERVER_TIME_TAG);
644                                int sessionExpirationDelay = (Integer)message.getHeader(SESSION_EXP_TAG);
645                                rescheduleSessionExpirationTask(serverTime, sessionExpirationDelay);
646                        }
647                }
648                else if (event instanceof IncomingMessageEvent<?>)
649                        sessionId = (String)((IncomingMessageEvent<?>)event).getMessage().getHeader(SESSION_ID_TAG);
650                
651                if (sessionId == null || !sessionId.equals(oldSessionId))
652                    log.info("Received new sessionId %s", sessionId);
653                
654                if (oldSessionId != null || sessionId != null) {
655            for (MessagingChannel messagingChannel : messagingChannelsByType.values())
656                            messagingChannel.setSessionId(sessionId);
657        }
658                
659                status.setConnected(true);
660        }
661
662    /**
663     * Callback called when a remoting fault is received
664     * @param event fault event
665     * @param emsg fault message
666     */
667        public void onFaultEvent(FaultEvent event, FaultMessage emsg) {
668                if (sessionExpirationFuture != null)
669                        sessionExpirationFuture.cancel(false);
670                                
671        String oldSessionId = sessionId;
672
673                sessionId = (String)event.getMessage().getHeader(SESSION_ID_TAG);
674                if (sessionId != null) {
675                        long serverTime = (Long)event.getMessage().getHeader(SERVER_TIME_TAG);
676                        int sessionExpirationDelay = (Integer)event.getMessage().getHeader(SESSION_EXP_TAG);
677                        rescheduleSessionExpirationTask(serverTime, sessionExpirationDelay);
678                }
679                
680        if (sessionId == null || !sessionId.equals(oldSessionId))
681            log.info("Received new sessionId %s", sessionId);
682        
683                if (oldSessionId != null || sessionId != null) {
684            for (MessagingChannel messagingChannel : messagingChannelsByType.values())
685                messagingChannel.setSessionId(sessionId);
686        }
687                
688        if (emsg != null && emsg.getCode().equals(Code.SERVER_CALL_FAILED))
689                status.setConnected(false);            
690        }
691
692    /**
693     * Callback called when a remoting failure is received
694     * @param event failure event
695     */
696        public void onIssueEvent(IssueEvent event) {
697        status.setConnected(false);            
698        }
699        
700        private final TransportStatusHandler statusHandler = new TransportStatusHandler() {
701                
702                private int busyCount = 0;
703                
704                @Override
705                public void handleIO(boolean active) {                  
706                        if (active)
707                                busyCount++;
708                        else
709                                busyCount--;
710                        status.setBusy(busyCount > 0);
711                        notifyIOListeners(status.isBusy());
712                }
713                
714                @Override
715                public void handleException(TransportException e) {
716                        log.warn(e, "Transport failed");
717                        notifyExceptionListeners(e);
718                }
719        };
720
721    /**
722     * Current remoting transport
723     * @return remoting transport
724     */
725        public Transport getRemotingTransport() {
726                return channelFactory != null ? channelFactory.getRemotingTransport() : null;
727        }
728
729    /**
730     * Current messaging transport
731     * @return messaging transport
732     */
733        public Transport getMessagingTransport() {
734                return channelFactory != null ? channelFactory.getMessagingTransport() : null;
735        }
736        
737        
738        /**
739         *      Implementation of login
740     *  Should not be called directly, called by {@link org.granite.client.tide.Identity#login}
741         *      
742         *      @param username user name
743         *  @param password password
744         */
745    public void login(String username, String password) {
746        remotingChannel.setCredentials(new UsernamePasswordCredentials(username, password));
747        for (MessagingChannel messagingChannel : messagingChannelsByType.values())
748            messagingChannel.setCredentials(new UsernamePasswordCredentials(username, password));
749    }
750
751        /**
752         *      Implementation of login using a specific charset for username/password encoding
753     *  Should not be called directly, called by {@link org.granite.client.tide.Identity#login}
754         *
755         *      @param username user name
756         *  @param password password
757         *  @param charset charset used for encoding
758         */
759    public void login(String username, String password, Charset charset) {
760        remotingChannel.setCredentials(new UsernamePasswordCredentials(username, password, charset));
761        for (MessagingChannel messagingChannel : messagingChannelsByType.values())
762            messagingChannel.setCredentials(new UsernamePasswordCredentials(username, password, charset));
763    }
764
765    /**
766     * Called by {@link org.granite.client.tide.Identity} after login has succeeded
767     */
768    public void afterLogin() {
769                log.info("Application session authenticated");
770                
771                context.getEventBus().raiseEvent(context, LOGIN);
772    }
773
774    /**
775     * Called by {@link org.granite.client.tide.Identity} after session has expired
776     */
777    public void sessionExpired() {
778                log.info("Application session expired");
779
780        if (remotingChannel.isAuthenticated())
781            remotingChannel.logout(false);
782        for (MessagingChannel messagingChannel : messagingChannelsByType.values()) {
783            if (messagingChannel.isAuthenticated())
784                messagingChannel.logout(false);
785        }
786
787                sessionId = null;
788                if (remotingChannel instanceof SessionAwareChannel)
789                    ((SessionAwareChannel)remotingChannel).setSessionId(null);
790        for (MessagingChannel messagingChannel : messagingChannelsByType.values())
791            messagingChannel.setSessionId(null);
792                
793                logoutState.sessionExpired();
794                
795                context.getEventBus().raiseEvent(context, SESSION_EXPIRED);             
796                context.getEventBus().raiseEvent(context, LOGOUT);              
797    }
798
799        /**
800         *      Implementation of logout
801     *  Should not be called directly, called by {@link org.granite.client.tide.Identity#logout}
802         *      
803         *      @param logoutObserver observer that will be notified of logout result
804         */
805        public void logout(final Observer logoutObserver) {
806                if (sessionExpirationFuture != null) {
807                        sessionExpirationFuture.cancel(false);
808                        sessionExpirationFuture = null;
809                }
810                
811                logoutState.logout(logoutObserver, new TimerTask() {
812                        @Override
813                        public void run() {
814                                log.info("Force session logout");
815                                logoutState.logout(logoutObserver);
816                                tryLogout();
817                        }
818                });
819                
820                context.getEventBus().raiseEvent(context, LOGOUT);
821                
822        tryLogout();
823    }
824        
825        /**
826         *      Notify the framework that it should wait for a async operation before effectively logging out.
827         *  Only if a logout has been requested.
828         */
829        public void checkWaitForLogout() {
830                logoutState.checkWait();
831        }
832        
833        /**
834         *      Called after all remote operations on a component are finished.
835         *  The actual logout is done when all remote operations on all components have been notified as finished.
836         */
837        public void tryLogout() {
838                if (logoutState.stillWaiting())
839                        return;
840                
841                if (logoutState.isSessionExpired()) {  // Don't remotely logout again if we detected a session expired
842                    logoutState.loggedOut(null);
843                    return;
844                }
845                
846                if (remotingChannel.isAuthenticated()) {
847                        remotingChannel.logout(new ResultFaultIssuesResponseListener() {
848                                @Override
849                                public void onResult(final ResultEvent event) {
850                                        context.callLater(new Runnable() {
851                                                public void run() {
852                                                        log.info("Application session logged out");
853
854                            new ResultHandler<Object>(ServerSession.this, null, "logout").handleResult(context, null, null, null);
855                                                        context.getContextManager().destroyContexts();
856                                                        
857                                                        logoutState.loggedOut(new TideResultEvent<Object>(context, ServerSession.this, null, event.getResult()));
858                                                }
859                                        });
860                                }
861        
862                                @Override
863                                public void onFault(final FaultEvent event) {
864                                        context.callLater(new Runnable() {
865                                                public void run() {
866                                                        log.error("Could not log out %s", event.getDescription());
867
868                            new FaultHandler<Object>(ServerSession.this, null, "logout").handleFault(context, event.getMessage());
869
870                                                Fault fault = new Fault(event.getCode(), event.getDescription(), event.getDetails());
871                                                fault.setContent(event.getMessage());
872                                                fault.setCause(event.getCause());                                       
873                                                        logoutState.loggedOut(new TideFaultEvent(context, ServerSession.this, null, fault, event.getExtended()));
874                                                }
875                                        });
876                                }
877                                
878                                @Override
879                                public void onIssue(final IssueEvent event) {
880                                        context.callLater(new Runnable() {
881                                                public void run() {
882                                                        log.error("Could not logout %s", event.getType());
883
884                            new FaultHandler<Object>(ServerSession.this, null, "logout").handleFault(context, null);
885                                                        
886                                                Fault fault = new Fault(Code.SERVER_CALL_FAILED, event.getType().name(), "");
887                                                        logoutState.loggedOut(new TideFaultEvent(context, ServerSession.this, null, fault, null));
888                                                }
889                                        });
890                                }
891                        });
892                }
893
894        for (MessagingChannel messagingChannel : messagingChannelsByType.values()) {
895                    if (messagingChannel != remotingChannel && messagingChannel.isAuthenticated())
896                            messagingChannel.logout();
897        }
898        }
899
900
901        private static class LogoutState extends Observable {
902                
903                private boolean logoutInProgress = false;
904                private int waitForLogout = 0;
905                private boolean sessionExpired = false;
906                private Timer logoutTimeout = null;
907                
908                public synchronized void logout(Observer logoutObserver, TimerTask forceLogout) {
909                        logout(logoutObserver);
910                        logoutTimeout = new Timer(true);
911                        logoutTimeout.schedule(forceLogout, 1000L);
912                }
913                
914                public synchronized void logout(Observer logoutObserver) {
915                        if (logoutObserver != null)
916                                addObserver(logoutObserver);
917                        if (!logoutInProgress) {
918                        logoutInProgress = true;
919                            waitForLogout = 1;
920                        }
921                }
922                
923                public synchronized void checkWait() {
924                        if (logoutInProgress)
925                                waitForLogout++;
926                }
927                
928                public synchronized boolean stillWaiting() {
929                        if (sessionExpired)
930                                return false;
931                        
932                        if (!logoutInProgress)
933                                return true;
934                        
935                        waitForLogout--;
936                        if (waitForLogout > 0)
937                                return true;
938                        
939                        return false;
940                }
941                
942                public boolean isSessionExpired() {
943                        return sessionExpired;
944                }
945                
946                public synchronized void loggedOut(TideRpcEvent event) {
947                        if (logoutTimeout != null) {
948                                logoutTimeout.cancel();
949                                logoutTimeout = null;
950                        }
951                        
952                        if (event != null) {
953                        setChanged();
954                        notifyObservers(event);
955                        deleteObservers();
956                        }
957                        
958                        logoutInProgress = false;
959                        waitForLogout = 0;
960                        sessionExpired = false;
961                }
962                
963                public synchronized void sessionExpired() {
964                        logoutInProgress = false;
965                        waitForLogout = 0;
966                        sessionExpired = true;
967                }
968        }
969
970
971    /**
972     * Status notified of network related events
973     */
974        public interface Status {
975
976        /**
977         * Network I/O busy
978         * @return true is busy
979         */
980                public boolean isBusy();
981
982        /**
983         * Set I/O busy, called by transport listeners
984         * @param busy true if busy
985         */
986                public void setBusy(boolean busy);
987
988        /**
989         * Network connected
990         * @return true if connected
991         */
992                public boolean isConnected();
993
994        /**
995         * Set connected state, called by transport listeners
996         * @param connected true if connected
997         */
998                public void setConnected(boolean connected);
999
1000        /**
1001         * Busy cursor enabled ?
1002         * @return true if busy cursor enabled
1003         */
1004                public boolean isShowBusyCursor();
1005
1006        /**
1007         * Enable/disable busy cursor
1008         * @param showBusyCursor true if enabled
1009         */
1010                public void setShowBusyCursor(boolean showBusyCursor);
1011        }
1012
1013        
1014        public static class DefaultStatus implements Status {
1015                
1016                private boolean showBusyCursor = true;
1017                
1018                private boolean connected = false;
1019                private boolean busy = false;
1020
1021                @Override
1022                public boolean isBusy() {
1023                        return busy;
1024                }
1025                
1026                public void setBusy(boolean busy) {
1027                        this.busy = busy;
1028                }
1029
1030                @Override
1031                public boolean isConnected() {
1032                        return connected;
1033                }
1034
1035                public void setConnected(boolean connected) {
1036                        this.connected = connected;
1037                }
1038
1039                @Override
1040                public boolean isShowBusyCursor() {
1041                        return showBusyCursor;
1042                }
1043
1044                @Override
1045                public void setShowBusyCursor(boolean showBusyCursor) {
1046                        this.showBusyCursor = showBusyCursor;                   
1047                }               
1048        }
1049        
1050        
1051        private long defaultTimeToLive = -1;
1052
1053    /**
1054     * Set default time to live on all channels
1055     * @param timeToLive time to live in milliseconds
1056     */
1057        public void setDefaultTimeToLive(long timeToLive) {
1058            defaultTimeToLive = timeToLive;
1059            
1060            if (channelFactory != null)
1061                channelFactory.setDefaultTimeToLive(timeToLive);
1062
1063        for (MessagingChannel messagingChannel : messagingChannelsByType.values())
1064            messagingChannel.setDefaultTimeToLive(timeToLive);
1065
1066            if (remotingChannel != null)
1067                remotingChannel.setDefaultTimeToLive(timeToLive);
1068        }
1069        
1070        private List<TransportIOListener> transportIOListeners = new ArrayList<TransportIOListener>();
1071        private List<TransportExceptionListener> transportExceptionListeners = new ArrayList<TransportExceptionListener>();
1072        
1073        public void addListener(TransportIOListener listener) {
1074                transportIOListeners.add(listener);
1075        }
1076        public void removeListener(TransportIOListener listener) {
1077                transportIOListeners.remove(listener);
1078        }
1079        
1080        public void addListener(TransportExceptionListener listener) {
1081                transportExceptionListeners.add(listener);
1082        }
1083        public void removeListener(TransportExceptionListener listener) {
1084                transportExceptionListeners.remove(listener);
1085        }
1086        
1087        public interface TransportIOListener {          
1088                public void handleIO(boolean busy);
1089        }
1090        
1091        public interface TransportExceptionListener {           
1092                public void handleException(TransportException e);
1093        }
1094        
1095        public void notifyIOListeners(boolean busy) {
1096                for (TransportIOListener listener : transportIOListeners)
1097                        listener.handleIO(busy);
1098        }
1099        
1100        public void notifyExceptionListeners(TransportException e) {
1101                for (TransportExceptionListener listener : transportExceptionListeners)
1102                        listener.handleException(e);
1103        }
1104}