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 }