001    /*
002      GRANITE DATA SERVICES
003      Copyright (C) 2012 GRANITE DATA SERVICES S.A.S.
004    
005      This file is part of Granite Data Services.
006    
007      Granite Data Services is free software; you can redistribute it and/or modify
008      it under the terms of the GNU Library General Public License as published by
009      the Free Software Foundation; either version 2 of the License, or (at your
010      option) any later version.
011    
012      Granite Data Services is distributed in the hope that it will be useful, but
013      WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
014      FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License
015      for more details.
016    
017      You should have received a copy of the GNU Library General Public License
018      along with this library; if not, see <http://www.gnu.org/licenses/>.
019    */
020    
021    package org.granite.client.tide.server;
022    
023    import java.net.URI;
024    import java.util.ArrayList;
025    import java.util.Collections;
026    import java.util.Comparator;
027    import java.util.Date;
028    import java.util.HashMap;
029    import java.util.List;
030    import java.util.Map;
031    import java.util.Observable;
032    import java.util.Observer;
033    import java.util.Set;
034    import java.util.Timer;
035    import java.util.TimerTask;
036    import java.util.concurrent.Executors;
037    import java.util.concurrent.ScheduledExecutorService;
038    import java.util.concurrent.ScheduledFuture;
039    import java.util.concurrent.TimeUnit;
040    
041    import javax.annotation.PostConstruct;
042    import javax.annotation.PreDestroy;
043    import javax.inject.Named;
044    
045    import org.granite.client.configuration.Configuration;
046    import org.granite.client.messaging.Consumer;
047    import org.granite.client.messaging.Producer;
048    import org.granite.client.messaging.RemoteClassScanner;
049    import org.granite.client.messaging.RemoteService;
050    import org.granite.client.messaging.ResultFaultIssuesResponseListener;
051    import org.granite.client.messaging.TopicAgent;
052    import org.granite.client.messaging.channel.ChannelFactory;
053    import org.granite.client.messaging.channel.MessagingChannel;
054    import org.granite.client.messaging.channel.RemotingChannel;
055    import org.granite.client.messaging.channel.SessionAwareChannel;
056    import org.granite.client.messaging.channel.UsernamePasswordCredentials;
057    import org.granite.client.messaging.events.Event;
058    import org.granite.client.messaging.events.FaultEvent;
059    import org.granite.client.messaging.events.IncomingMessageEvent;
060    import org.granite.client.messaging.events.IssueEvent;
061    import org.granite.client.messaging.events.ResultEvent;
062    import org.granite.client.messaging.messages.responses.FaultMessage;
063    import org.granite.client.messaging.messages.responses.FaultMessage.Code;
064    import org.granite.client.messaging.messages.responses.ResultMessage;
065    import org.granite.client.messaging.transport.Transport;
066    import org.granite.client.messaging.transport.TransportException;
067    import org.granite.client.messaging.transport.TransportStatusHandler;
068    import org.granite.client.messaging.transport.apache.ApacheAsyncTransport;
069    import org.granite.client.messaging.transport.jetty.JettyWebSocketTransport;
070    import org.granite.client.tide.BeanManager;
071    import org.granite.client.tide.Context;
072    import org.granite.client.tide.ContextAware;
073    import org.granite.client.tide.Identity;
074    import org.granite.client.tide.PlatformConfigurable;
075    import org.granite.client.tide.PropertyHolder;
076    import org.granite.client.tide.data.EntityManager;
077    import org.granite.client.tide.data.EntityManager.Update;
078    import org.granite.client.tide.data.spi.MergeContext;
079    import org.granite.logging.Logger;
080    import org.granite.messaging.amf.io.convert.Converters;
081    import org.granite.tide.Expression;
082    import org.granite.tide.invocation.ContextResult;
083    import org.granite.tide.invocation.ContextUpdate;
084    import org.granite.tide.invocation.InvocationResult;
085    import org.granite.util.ContentType;
086    
087    
088    /**
089     * @author William DRAI
090     */
091    @PlatformConfigurable
092    @Named
093    public class ServerSession implements ContextAware {
094    
095            private static Logger log = Logger.getLogger(ServerSession.class);
096            
097            public static final String SERVER_TIME_TAG = "org.granite.time";
098            public static final String SESSION_ID_TAG = "org.granite.sessionId";
099            public static final String SESSION_EXP_TAG = "org.granite.sessionExp";
100        public static final String CONVERSATION_TAG = "conversationId";
101        public static final String CONVERSATION_PROPAGATION_TAG = "conversationPropagation";
102        public static final String IS_LONG_RUNNING_CONVERSATION_TAG = "isLongRunningConversation";
103        public static final String WAS_LONG_RUNNING_CONVERSATION_ENDED_TAG = "wasLongRunningConversationEnded";
104        public static final String WAS_LONG_RUNNING_CONVERSATION_CREATED_TAG = "wasLongRunningConversationCreated";
105        public static final String IS_FIRST_CALL_TAG = "org.granite.client.tide.isFirstCall";
106        public static final String IS_FIRST_CONVERSATION_CALL_TAG = "org.granite.client.tide.isFirstConversationCall";
107            
108        public static final String CONTEXT_RESULT = "org.granite.tide.result";
109        public static final String CONTEXT_FAULT = "org.granite.tide.fault";
110        
111            public static final String LOGIN = "org.granite.client.tide.login";
112            public static final String LOGOUT = "org.granite.client.tide.logout";
113            public static final String SESSION_EXPIRED = "org.granite.client.tide.sessionExpired";
114            
115            private static final String DEFAULT_REMOTING_URL_MAPPING = "/graniteamf/amf.txt";
116            private static final String DEFAULT_COMET_URL_MAPPING = "/gravityamf/amf.txt";
117            private static final String DEFAULT_WEBSOCKET_URL_MAPPING = "/websocketamf/amf";
118        
119        
120            @SuppressWarnings("unused")
121            private boolean confChanged = false;
122            private ContentType contentType = ContentType.AMF;
123            private boolean useWebSocket = true;
124        private Transport remotingTransport = null;
125        private Transport messagingTransport = null;
126        private String protocol = "http";
127        private String contextRoot = "";
128        private String serverName = null;
129        private int serverPort = 0;
130        private String graniteUrlMapping = DEFAULT_REMOTING_URL_MAPPING;            // .txt for stupid bug in IE8
131        private String gravityUrlMapping = DEFAULT_COMET_URL_MAPPING;
132        
133        private URI graniteURI;
134            private URI gravityURI;
135        
136            private Context context = null;
137        private TrackingContext trackingContext = new TrackingContext();
138            
139            private Status status = new DefaultStatus();
140            
141            private String sessionId = null;
142            private boolean isFirstCall = true;
143            
144            private LogoutState logoutState = new LogoutState();
145                    
146            private String destination = "server";
147            private Configuration configuration = null;
148        private RemotingChannel remotingChannel;
149            private MessagingChannel messagingChannel;
150            protected Map<String, RemoteService> remoteServices = new HashMap<String, RemoteService>();
151            protected Map<String, TopicAgent> topicAgents = new HashMap<String, TopicAgent>();
152            private RemoteClassScanner remoteClassConfigurator = new RemoteClassScanner();
153            
154            
155        public ServerSession() throws Exception {
156            // Used for testing
157        }
158        
159        public ServerSession(String contextRoot, String serverName, int serverPort) throws Exception {
160            this(null, "http", contextRoot, serverName, serverPort, null, null);
161        }
162    
163        public ServerSession(String contextRoot, String serverName, int serverPort, String graniteUrlMapping, String gravityUrlMapping) throws Exception {
164            this(null, "http", contextRoot, serverName, serverPort, graniteUrlMapping, gravityUrlMapping);
165        }
166        
167        public ServerSession(String destination, String contextRoot, String serverName, int serverPort) throws Exception {
168            this(destination, "http", contextRoot, serverName, serverPort, null, null);
169        }
170    
171        public ServerSession(String destination, String contextRoot, String serverName, int serverPort, String graniteUrlMapping, String gravityUrlMapping) throws Exception {
172            this(destination, "http", contextRoot, serverName, serverPort, graniteUrlMapping, gravityUrlMapping);
173        }
174        
175        public ServerSession(String destination, String protocol, String contextRoot, String serverName, int serverPort, String graniteUrlMapping, String gravityUrlMapping) throws Exception {
176            super();
177            if (destination != null)
178                    this.destination = destination;
179            this.protocol = protocol;
180            this.contextRoot = contextRoot;
181            this.serverName = serverName;
182            this.serverPort = serverPort;
183            if (graniteUrlMapping != null)
184                    this.graniteUrlMapping = graniteUrlMapping;
185            if (gravityUrlMapping != null)
186                    this.gravityUrlMapping = gravityUrlMapping;
187        }
188    
189        public ContentType getContentType() {
190                    return contentType;
191            }
192    
193            public void setContentType(ContentType contentType) {
194                    if (contentType == null)
195                            throw new NullPointerException("contentType cannot be null");
196                    this.contentType = contentType;
197            }
198    
199            public void setContextRoot(String contextRoot) {
200            this.contextRoot = contextRoot;
201            confChanged = true;
202        }
203        
204        public void setProtocol(String protocol) {
205            this.protocol = protocol;
206            confChanged = true;
207        }
208        
209        public void setServerName(String serverName) {
210            this.serverName = serverName;
211            confChanged = true;
212        }
213        
214        public void setServerPort(int serverPort) {
215            this.serverPort = serverPort;
216            confChanged = true;
217        }
218        
219        public void setGraniteUrlMapping(String graniteUrlMapping) {
220            this.graniteUrlMapping = graniteUrlMapping;
221            confChanged = true;
222        }
223        
224        public void setGravityUrlMapping(String gravityUrlMapping) {
225            this.gravityUrlMapping = gravityUrlMapping;
226            confChanged = true;
227        }
228        
229            public void setDestination(String destination) {
230                    this.destination = destination;
231            }
232            
233            public void setContext(Context context) {
234                    this.context = context;
235            }
236            
237            public TrackingContext getTrackingContext() {
238                    return trackingContext;
239            }
240            
241            public void setStatus(Status status) {
242                    this.status = status;
243            }
244            
245            public Status getStatus() {
246                    return status;
247            }
248            
249            public void setUseWebSocket(boolean useWebSocket) {
250                    this.useWebSocket = useWebSocket;
251                    if (useWebSocket && DEFAULT_COMET_URL_MAPPING.equals(gravityUrlMapping))
252                            this.gravityUrlMapping = DEFAULT_WEBSOCKET_URL_MAPPING;
253            }
254            
255            public void setRemotingTransport(Transport transport) {
256                    this.remotingTransport = transport;
257            }
258            
259            public void setMessagingTransport(Transport transport) {
260                    this.messagingTransport = transport;
261            }
262            
263            public void setConfiguration(Configuration configuration) {
264                    this.configuration = configuration;
265            }
266            
267            public void addRemoteClassPackage(String packageName) {         
268                    remoteClassConfigurator.addPackageName(packageName);
269            }
270            
271            public void setRemoteClassPackage(Set<String> packageNames) {
272                    remoteClassConfigurator.setPackageNames(packageNames);
273            }
274            
275            public Converters getConverters() {
276                    return configuration.getGraniteConfig().getConverters();
277            }
278            
279            @PostConstruct
280            public void start() throws Exception {
281                    if (remotingTransport == null)
282                            remotingTransport = new ApacheAsyncTransport();
283                    
284                    if (messagingTransport == null)
285                            messagingTransport = useWebSocket ? new JettyWebSocketTransport() : remotingTransport;
286                    
287                    remotingTransport.setStatusHandler(statusHandler);
288                    remotingTransport.start();
289                    if (messagingTransport != remotingTransport) {
290                            messagingTransport.setStatusHandler(statusHandler);
291                            messagingTransport.start();
292                    }
293                    
294                    configuration.addConfigurator(remoteClassConfigurator);
295                    configuration.load();
296                    
297                    ChannelFactory factory = new ChannelFactory(contentType);
298                    
299                    graniteURI = new URI(protocol + "://" + this.serverName + (this.serverPort > 0 ? ":" + this.serverPort : "") + this.contextRoot + this.graniteUrlMapping);
300                    remotingChannel = factory.newRemotingChannel(remotingTransport, configuration, "graniteamf", graniteURI, 1);
301    
302                    if (useWebSocket)
303                            gravityURI = new URI(protocol.replace("http", "ws") + "://" + this.serverName + (this.serverPort > 0 ? ":" + this.serverPort : "") + this.contextRoot + this.gravityUrlMapping);
304                    else
305                            gravityURI = new URI(protocol + "://" + this.serverName + (this.serverPort > 0 ? ":" + this.serverPort : "") + this.contextRoot + this.gravityUrlMapping);
306                    messagingChannel = factory.newMessagingChannel(messagingTransport, configuration, "gravityamf", gravityURI);
307            }
308            
309            @PreDestroy
310            public void stop()throws Exception {
311                    if (remotingTransport != null)
312                            remotingTransport.stop();               
313                    remotingChannel = null;
314                    
315                    if (messagingTransport != null && messagingTransport != remotingTransport)
316                            messagingTransport.stop();              
317                    messagingChannel = null;
318            }
319            
320            
321            public static interface ServiceFactory {
322                    
323                    public RemoteService newRemoteService(RemotingChannel remotingChannel, String destination);
324                    
325                    public Producer newProducer(MessagingChannel messagingChannel, String destination, String topic);
326                    
327                    public Consumer newConsumer(MessagingChannel messagingChannel, String destination, String topic);
328            }
329            
330            private static class DefaultServiceFactory implements ServiceFactory {
331                    
332                    @Override
333                    public RemoteService newRemoteService(RemotingChannel remotingChannel, String destination) {
334                            return new RemoteService(remotingChannel, destination);
335                    }
336                    
337                    @Override
338                    public Producer newProducer(MessagingChannel messagingChannel, String destination, String topic) {
339                            return new Producer(messagingChannel, destination, topic);
340                    }
341                    
342                    @Override
343                    public Consumer newConsumer(MessagingChannel messagingChannel, String destination, String topic) {
344                            return new Consumer(messagingChannel, destination, topic);
345                    }
346            }
347            
348            private ServiceFactory serviceFactory = new DefaultServiceFactory();
349            
350            public void setServiceFactory(ServiceFactory serviceFactory) {
351                    this.serviceFactory = serviceFactory;
352            }
353            
354            public RemoteService getRemoteService() {
355                    return getRemoteService(destination);
356            }
357            public synchronized RemoteService getRemoteService(String destination) {
358                    if (remotingChannel == null)
359                            throw new IllegalStateException("Channel not defined for server session");
360                    
361                    RemoteService remoteService = remoteServices.get(destination);
362                    if (remoteService == null) {
363                            remoteService = serviceFactory.newRemoteService(remotingChannel, destination);
364                            remoteServices.put(destination, remoteService);
365                    }
366                    return remoteService;
367            }
368    
369            public synchronized Consumer getConsumer(String destination, String topic) {
370                    if (messagingChannel == null)
371                            throw new IllegalStateException("Channel not defined for server session");
372                    
373                    String key = destination + '@' + topic;
374                    TopicAgent consumer = topicAgents.get(key);
375                    if (consumer == null) {
376                            consumer = serviceFactory.newConsumer(messagingChannel, destination, topic);
377                            topicAgents.put(key, consumer);
378                    }
379                    return consumer instanceof Consumer ? (Consumer)consumer : null;
380            }
381    
382            public synchronized Producer getProducer(String destination, String topic) {
383                    if (messagingChannel == null)
384                            throw new IllegalStateException("Channel not defined for server session");
385                    
386                    String key = destination + '@' + topic;
387                    TopicAgent producer = topicAgents.get(key);
388                    if (producer == null) {
389                            producer = serviceFactory.newProducer(messagingChannel, destination, topic);
390                            topicAgents.put(key, producer);
391                    }
392                    return producer instanceof Producer ? (Producer)producer : null;
393            }
394            
395            public boolean isFirstCall() {
396                    return isFirstCall;
397            }
398            
399            public String getSessionId() {
400                    return sessionId;
401            }
402            
403            public boolean isLogoutInProgress() {
404                    return logoutState.logoutInProgress;
405            }
406            
407            public void trackCall() {
408                    isFirstCall = false;
409            }
410            
411            private ScheduledExecutorService sessionExpirationTimer = Executors.newSingleThreadScheduledExecutor();
412            private ScheduledFuture<?> sessionExpirationFuture = null;
413            
414            private Runnable sessionExpirationTask = new Runnable() {
415                    @Override
416                    public void run() {
417                            Identity identity = context.byType(Identity.class);
418                            identity.checkLoggedIn(null);
419                    }
420            };
421            
422            private void rescheduleSessionExpirationTask(long serverTime, int sessionExpirationDelay) {
423                    Identity identity = context.byType(Identity.class);
424                    if (identity == null || !identity.isLoggedIn()) // No session expiration tracking if user not logged in
425                            return;
426                    
427                    long clientOffset = serverTime - new Date().getTime();
428                    if (sessionExpirationFuture != null)
429                            sessionExpirationFuture.cancel(false);
430                    
431                    sessionExpirationFuture = sessionExpirationTimer.schedule(sessionExpirationTask, clientOffset + sessionExpirationDelay*1000L + 1500L, TimeUnit.MILLISECONDS);
432            }
433            
434            public void handleResultEvent(Event event) {
435                    if (event instanceof ResultEvent) {
436                            ResultMessage message = ((ResultEvent)event).getMessage();
437                            sessionId = (String)message.getHeader(SESSION_ID_TAG);
438                            if (sessionId != null) {
439                                    long serverTime = (Long)message.getHeader(SERVER_TIME_TAG);
440                                    int sessionExpirationDelay = (Integer)message.getHeader(SESSION_EXP_TAG);
441                                    rescheduleSessionExpirationTask(serverTime, sessionExpirationDelay);
442                            }
443                    }
444                    else if (event instanceof IncomingMessageEvent<?>)
445                            sessionId = (String)((IncomingMessageEvent<?>)event).getMessage().getHeader(SESSION_ID_TAG);
446                    
447                    if (messagingChannel != null && sessionId != null && messagingChannel instanceof SessionAwareChannel)
448                            ((SessionAwareChannel)messagingChannel).setSessionId(sessionId);
449                    isFirstCall = false;
450                    status.setConnected(true);
451            }
452            
453            public void handleFaultEvent(FaultEvent event, FaultMessage emsg) {
454                    sessionId = (String)event.getMessage().getHeader(SESSION_ID_TAG);
455                    if (sessionId != null) {
456                            long serverTime = (Long)event.getMessage().getHeader(SERVER_TIME_TAG);
457                            int sessionExpirationDelay = (Integer)event.getMessage().getHeader(SESSION_EXP_TAG);
458                            rescheduleSessionExpirationTask(serverTime, sessionExpirationDelay);
459                    }
460                    
461                    if (messagingChannel != null && sessionId != null && messagingChannel instanceof SessionAwareChannel)
462                            ((SessionAwareChannel)messagingChannel).setSessionId(sessionId);
463                    
464            if (emsg != null && emsg.getCode().equals(Code.SERVER_CALL_FAILED))
465                    status.setConnected(false);            
466            }
467            
468            private final TransportStatusHandler statusHandler = new TransportStatusHandler() {
469                    
470                    private int busyCount = 0;
471                    
472                    @Override
473                    public void handleIO(boolean active) {                  
474                            if (active)
475                                    busyCount++;
476                            else
477                                    busyCount--;
478                            status.setBusy(busyCount > 0);
479                            notifyIOListeners(status.isBusy());
480                    }
481                    
482                    @Override
483                    public void handleException(TransportException e) {
484                            log.warn(e, "Transport failed");
485                            notifyExceptionListeners(e);
486                    }
487            };
488            
489            
490        /**
491         *  @private
492         *  Implementation of component invocation
493         *  
494         *  @param component component proxy
495         *  @param op remote operation
496         *  @param args array of operation arguments
497         *  @param responder Tide responder
498         *  @param withContext send additional context with the call
499         *  @param handler optional operation handler
500         * 
501         *  @return token for the remote operation
502         */
503            
504        
505        public void login(String username, String password) {
506            remotingChannel.setCredentials(new UsernamePasswordCredentials(username, password));
507            messagingChannel.setCredentials(new UsernamePasswordCredentials(username, password));
508        }
509        
510        public void afterLogin() {
511                    log.info("Application session authenticated");
512                    
513                    context.getEventBus().raiseEvent(context, LOGIN);
514        }
515        
516        public void sessionExpired() {
517                    log.info("Application session expired");
518                    
519                    sessionId = null;
520                    isFirstCall = true;
521                    
522                    logoutState.sessionExpired();
523                    
524                    context.getEventBus().raiseEvent(context, SESSION_EXPIRED);             
525                    context.getEventBus().raiseEvent(context, LOGOUT);              
526        }
527        
528            /**
529             *      @private
530             *      Implementation of logout
531             *      
532             *      @param ctx current context
533             *  @param componentName component name of identity
534             */
535            public void logout(final Observer logoutObserver) {
536                    if (sessionExpirationFuture != null) {
537                            sessionExpirationFuture.cancel(false);
538                            sessionExpirationFuture = null;
539                    }
540                    
541                    logoutState.logout(logoutObserver, new TimerTask() {
542                            @Override
543                            public void run() {
544                                    log.info("Force session logout");
545                                    logoutState.logout(logoutObserver);
546                                    tryLogout();
547                            }
548                    });
549                    
550                    context.getEventBus().raiseEvent(context, LOGOUT);
551                    
552            tryLogout();
553        }
554            
555            /**
556             *      Notify the framework that it should wait for a async operation before effectively logging out.
557             *  Only if a logout has been requested.
558             */
559            public void checkWaitForLogout() {
560                    isFirstCall = false;
561                    
562                    logoutState.checkWait();
563            }
564            
565            /**
566             *      Try logout. Should be called after all remote operations on a component are finished.
567             *  The effective logout is done when all remote operations on all components have been notified as finished.
568             */
569            public void tryLogout() {
570                    if (logoutState.stillWaiting())
571                            return;
572                    
573                    if (remotingChannel.isAuthenticated()) {
574                            remotingChannel.logout(new ResultFaultIssuesResponseListener() {
575                                    @Override
576                                    public void onResult(final ResultEvent event) {
577                                            context.callLater(new Runnable() {
578                                                    public void run() {
579                                                            log.info("Application session logged out");
580                                                            
581                                                            handleResult(context, null, "logout", null, null, null);
582                                                            context.getContextManager().destroyContexts(false);
583                                                            
584                                                            logoutState.loggedOut(new TideResultEvent<Object>(context, ServerSession.this, null, event.getResult()));
585                                                    }
586                                            });
587                                    }
588            
589                                    @Override
590                                    public void onFault(final FaultEvent event) {
591                                            context.callLater(new Runnable() {
592                                                    public void run() {
593                                                            log.error("Could not log out %s", event.getDescription());
594                                                            
595                                                            handleFault(context, null, "logout", event.getMessage());
596                                                            
597                                                    Fault fault = new Fault(event.getCode(), event.getDescription(), event.getDetails());
598                                                    fault.setContent(event.getMessage());
599                                                    fault.setCause(event.getCause());                                       
600                                                            logoutState.loggedOut(new TideFaultEvent(context, ServerSession.this, null, fault, event.getExtended()));
601                                                    }
602                                            });
603                                    }
604                                    
605                                    @Override
606                                    public void onIssue(final IssueEvent event) {
607                                            context.callLater(new Runnable() {
608                                                    public void run() {
609                                                            log.error("Could not logout %s", event.getType());
610                                                            
611                                                            handleFault(context, null, "logout", null);
612                                                            
613                                                    Fault fault = new Fault(Code.SERVER_CALL_FAILED, event.getType().name(), "");
614                                                            logoutState.loggedOut(new TideFaultEvent(context, ServerSession.this, null, fault, null));
615                                                    }
616                                            });
617                                    }
618                            });
619                    }
620                    
621                    if (messagingChannel != remotingChannel && messagingChannel.isAuthenticated())
622                            messagingChannel.logout();
623            }
624            
625            
626        /**
627         *  @private  
628         *  (Almost) abstract method: manages a remote call result
629         *  This should be called by the implementors at the end of the result processing
630         * 
631         *  @param componentName name of the target component
632         *  @param operation name of the called operation
633         *  @param ires invocation result object
634         *  @param result result object
635         *  @param mergeWith previous value with which the result will be merged
636         */
637        public void handleResult(Context context, String componentName, String operation, InvocationResult invocationResult, Object result, Object mergeWith) {
638            trackingContext.clearPendingUpdates();
639            
640            log.debug("result {0}", result);
641            
642            List<ContextUpdate> resultMap = null;
643            List<Update> updates = null;
644            
645            EntityManager entityManager = context.getEntityManager();
646            BeanManager beanManager = context.getBeanManager();
647            
648            try {
649                trackingContext.setEnabled(false);
650                
651                // Clear flash context variable for Grails/Spring MVC
652                context.remove("flash");
653                
654                MergeContext mergeContext = entityManager.initMerge();
655                mergeContext.setServerSession(this);
656                
657                boolean mergeExternal = true;
658                if (invocationResult != null) {
659                    mergeExternal = invocationResult.getMerge();
660                    
661                    if (invocationResult.getUpdates() != null && invocationResult.getUpdates().length > 0) {
662                        updates = new ArrayList<Update>(invocationResult.getUpdates().length);
663                        for (Object[] u : invocationResult.getUpdates())
664                            updates.add(Update.forUpdate((String)u[0], u[1]));
665                        entityManager.handleUpdates(mergeContext, null, updates);
666                    }
667                    
668                    // Handle scope changes
669                    // TODO: merge context variables
670        //            if (_componentStore.isComponentInEvent(componentName) && invocationResult.scope == Tide.SCOPE_SESSION && !meta_isGlobal) {
671        //                var instance:Object = this[componentName];
672        //                this[componentName] = null;
673        //                _componentStore.getDescriptor(componentName).scope = Tide.SCOPE_SESSION;
674        //                this[componentName] = instance;
675        //            }
676                    
677        //            componentRegistry.setScope(componentName, ScopeType.values()[invocationResult.getScope()]);
678        //            componentRegistry.setRestrict(componentName, invocationResult.getRestrict() ? RestrictMode.YES : RestrictMode.NO);
679                    
680                    resultMap = invocationResult.getResults();
681                    
682                    if (resultMap != null) {
683                        log.debug("result conversationId {0}", context.getContextId());
684                        
685                        // Order the results by container, i.e. 'person.contacts' has to be evaluated after 'person'
686                        Collections.sort(resultMap, RESULTS_COMPARATOR);
687                        
688                        for (int k = 0; k < resultMap.size(); k++) {
689                            ContextUpdate r = resultMap.get(k);
690                            Object val = r.getValue();
691                            
692                            log.debug("update expression {0}: {1}", r, val);
693        
694                            String compName = r.getComponentName();
695                            // TODO: merge context variables
696        //                    if (compName == null && val != null) {
697        //                        var t:Type = Type.forInstance(val);
698        //                        compName = ComponentStore.internalNameForTypedComponent(t.name + '_' + t.id);
699        //                    }
700                            
701                            trackingContext.addLastResult(compName 
702                                    + (r.getComponentClassName() != null ? "(" + r.getComponentClassName() + ")" : "") 
703                                    + (r.getExpression() != null ? "." + r.getExpression() : ""));
704                            
705                            // TODO: merge context variables
706        //                    if (val != null) {
707        //                        if (_componentStore.getDescriptor(compName).restrict == Tide.RESTRICT_UNKNOWN)
708        //                            _componentStore.getDescriptor(compName).restrict = r.restrict ? Tide.RESTRICT_YES : Tide.RESTRICT_NO;
709        //                        
710        //                        if (_componentStore.getDescriptor(compName).scope == Tide.SCOPE_UNKNOWN)
711        //                            _componentStore.getDescriptor(compName).scope = r.scope;
712        //                    }
713        //                    _componentStore.setComponentGlobal(compName, true);
714                            
715                            Object obj = context.byNameNoProxy(compName);
716                            String[] p = r.getExpression() != null ? r.getExpression().split("\\.") : null;
717                            if (p != null && p.length > 1) {
718                                for (int i = 0; i < p.length-1; i++)
719                                    obj = beanManager.getProperty(obj, p[i]);
720                            }
721        //                    else if (p.length == 0)
722        //                        _componentStore.setComponentRemoteSync(compName, Tide.SYNC_BIDIRECTIONAL);
723                            
724                            Object previous = null;
725                            String propName = null;
726                            if (p != null && p.length > 0) {
727                                propName = p[p.length-1];
728                                
729                                if (obj instanceof PropertyHolder)
730                                    previous = beanManager.getProperty(((PropertyHolder)obj).getObject(), propName);
731                                else if (obj != null)
732                                    previous = beanManager.getProperty(obj, propName);
733                            }
734                            else
735                                previous = obj;
736                            
737                            // Don't merge with temporary properties
738                            // TODO: merge context variables
739                            if (previous instanceof Component) //  || previous instanceof ComponentProperty)
740                                previous = null;
741                            
742                            Expression res = new ContextResult(r.getComponentName(), r.getExpression());
743                            // TODO: merge context variables
744        //                    var res:IExpression = ComponentStore.isInternalNameForTypedComponent(compName)
745        //                        ? new TypedContextExpression(compName, r.expression)
746        //                        : new ContextResult(r.componentName, r.expression);
747                            
748        //                    if (!isGlobal() && r.getScope() == ScopeType.SESSION.ordinal())
749        //                        val = parentContext.getEntityManager().mergeExternal(val, previous, res);
750        //                    else
751                            val = entityManager.mergeExternal(mergeContext, val, previous, res, null, null, null, false);
752                            
753                            if (propName != null) {
754                                if (obj instanceof PropertyHolder) {
755                                    ((PropertyHolder)obj).setProperty(propName, val);
756                                }
757                                else if (obj != null)
758                                    beanManager.setProperty(obj, propName, val);
759                            }
760                            else
761                                context.set(compName, val);
762                        }
763                    }
764                }
765                
766                // Merges final result object
767                if (result != null) {
768                    if (mergeExternal)
769                        result = entityManager.mergeExternal(mergeContext, result, mergeWith, null, null, null, null, false);
770                    else
771                        log.debug("skipped merge of remote result");
772                    if (invocationResult != null)
773                        invocationResult.setResult(result);
774                }
775            }
776            finally {
777                MergeContext.destroy(entityManager);
778                
779                trackingContext.setEnabled(true);
780            }
781    
782            // TODO: Seam 2 status messages support
783    //        if (componentRegistry.isComponent("statusMessages"))
784    //            get("statusMessages").setFromServer(invocationResult);
785            
786            // Dispatch received data update events         
787            if (invocationResult != null) {
788                trackingContext.removeResults(resultMap);
789                
790                // Dispatch received data update events
791                if (updates != null)
792                    entityManager.raiseUpdateEvents(context, updates);
793                
794                // TODO: dispatch received context events
795    //            List<ContextEvent> events = invocationResult.getEvents();
796    //            if (events != null && events.size() > 0) {
797    //                for (ContextEvent event : events) {
798    //                    if (event.params[0] is Event)
799    //                        meta_dispatchEvent(event.params[0] as Event);
800    //                    else if (event.isTyped())
801    //                        meta_internalRaiseEvent("$TideEvent$" + event.eventType, event.params);
802    //                    else
803    //                        _tide.invokeObservers(this, TideModuleContext.currentModulePrefix, event.eventType, event.params);
804    //                }
805    //            }
806            }
807            
808            log.debug("result merged into local context");
809        }
810    
811        /**
812         *  @private 
813         *  Abstract method: manages a remote call fault
814         * 
815         *  @param componentName name of the target component
816         *  @param operation name of the called operation
817         *  @param emsg error message
818         */
819        public void handleFault(Context context, String componentName, String operation, FaultMessage emsg) {
820            trackingContext.clearPendingUpdates();
821    
822            // TODO: Seam 2 status messages support
823    //        if (emsg != null && emsg.getExtendedData() != null && componentRegistry.isComponent("statusMessages"))
824    //            get("statusMessages").setFromServer(emsg.getExtendedData());
825        }
826        
827        private static final ResultsComparator RESULTS_COMPARATOR = new ResultsComparator();
828            
829            
830            private static class LogoutState extends Observable {
831                    
832                    private boolean logoutInProgress = false;
833                    private int waitForLogout = 0;
834                    private boolean sessionExpired = false;
835                    private Timer logoutTimeout = null;
836                    
837                    public synchronized void logout(Observer logoutObserver, TimerTask forceLogout) {
838                            logout(logoutObserver);
839                            logoutTimeout = new Timer(true);
840                            logoutTimeout.schedule(forceLogout, 1000L);
841                    }
842                    
843                    public synchronized void logout(Observer logoutObserver) {
844                            addObserver(logoutObserver);
845                    logoutInProgress = true;
846                        waitForLogout = 1;
847                    }
848                    
849                    public synchronized void checkWait() {
850                            if (logoutInProgress)
851                                    waitForLogout++;
852                    }
853                    
854                    public synchronized boolean stillWaiting() {
855                            if (sessionExpired)
856                                    return false;
857                            
858                            if (!logoutInProgress)
859                                    return true;
860                            
861                            waitForLogout--;
862                            if (waitForLogout > 0)
863                                    return true;
864                            
865                            return false;
866                    }
867                    
868                    @SuppressWarnings("unused")
869                    public boolean isSessionExpired() {
870                            return sessionExpired;
871                    }
872                    
873                    public synchronized void loggedOut(TideRpcEvent event) {
874                            if (logoutTimeout != null) {
875                                    logoutTimeout.cancel();
876                                    logoutTimeout = null;
877                            }
878                            
879                            setChanged();
880                            notifyObservers(event);
881                            deleteObservers();
882                            
883                            logoutInProgress = false;
884                            waitForLogout = 0;
885                            sessionExpired = false;
886                    }
887                    
888                    public synchronized void sessionExpired() {
889                            logoutInProgress = false;
890                            waitForLogout = 0;
891                            sessionExpired = true;
892                    }
893            }
894            
895            
896            public interface Status {
897    
898                    public boolean isBusy();
899                    
900                    public void setBusy(boolean busy);
901                    
902                    public boolean isConnected();
903                    
904                    public void setConnected(boolean connected);
905                    
906                    public boolean isShowBusyCursor();
907                    
908                    public void setShowBusyCursor(boolean showBusyCursor);
909            }
910    
911            
912            public static class DefaultStatus implements Status {
913                    
914                    private boolean showBusyCursor = true;
915                    
916                    private boolean connected = false;
917                    private boolean busy = false;
918    
919                    @Override
920                    public boolean isBusy() {
921                            return busy;
922                    }
923                    
924                    public void setBusy(boolean busy) {
925                            this.busy = busy;
926                    }
927    
928                    @Override
929                    public boolean isConnected() {
930                            return connected;
931                    }
932    
933                    public void setConnected(boolean connected) {
934                            this.connected = connected;
935                    }
936    
937                    @Override
938                    public boolean isShowBusyCursor() {
939                            return showBusyCursor;
940                    }
941    
942                    @Override
943                    public void setShowBusyCursor(boolean showBusyCursor) {
944                            this.showBusyCursor = showBusyCursor;                   
945                    }               
946            }
947            
948            
949            private List<TransportIOListener> transportIOListeners = new ArrayList<TransportIOListener>();
950            private List<TransportExceptionListener> transportExceptionListeners = new ArrayList<TransportExceptionListener>();
951            
952            public void addListener(TransportIOListener listener) {
953                    transportIOListeners.add(listener);
954            }
955            public void removeListener(TransportIOListener listener) {
956                    transportIOListeners.remove(listener);
957            }
958            
959            public void addListener(TransportExceptionListener listener) {
960                    transportExceptionListeners.add(listener);
961            }
962            public void removeListener(TransportExceptionListener listener) {
963                    transportExceptionListeners.remove(listener);
964            }
965            
966            public interface TransportIOListener {          
967                    public void handleIO(boolean busy);
968            }
969            
970            public interface TransportExceptionListener {           
971                    public void handleException(TransportException e);
972            }
973            
974            public void notifyIOListeners(boolean busy) {
975                    for (TransportIOListener listener : transportIOListeners)
976                            listener.handleIO(busy);
977            }
978            
979            public void notifyExceptionListeners(TransportException e) {
980                    for (TransportExceptionListener listener : transportExceptionListeners)
981                            listener.handleException(e);
982            }
983        
984            
985        /**
986         *  @private
987         *  Comparator for expression evaluation ordering
988         * 
989         *  @param r1 expression 1
990         *  @param r2 expression 2
991         *  @param fields unused
992         * 
993         *  @return comparison value
994         */
995        private static class ResultsComparator implements Comparator<ContextUpdate> {
996            
997            public int compare(ContextUpdate r1, ContextUpdate r2) {
998                
999                if (r1.getComponentClassName() != null && r2.getComponentClassName() != null && !r1.getComponentClassName().equals(r2.getComponentClassName()))
1000                    return r1.getComponentClassName().compareTo(r2.getComponentClassName());
1001                
1002                if (r1.getComponentName() != r2.getComponentName())
1003                    return r1.getComponentName().compareTo(r2.getComponentName());
1004                
1005                if (r1.getExpression() == null)
1006                    return r2.getExpression() == null ? 0 : -1;
1007                
1008                if (r2.getExpression() == null)
1009                    return 1;
1010                
1011                if (r1.getExpression().equals(r2.getExpression()))
1012                    return 0;
1013                
1014                if (r1.getExpression().indexOf(r2.getExpression()) == 0)
1015                    return 1;
1016                if (r2.getExpression().indexOf(r1.getExpression()) == 0)
1017                    return -1;
1018                
1019                return r1.getExpression().compareTo(r2.getExpression()) < 0 ? -1 : 0;
1020            }
1021        }
1022    }