001 /*
002 GRANITE DATA SERVICES
003 Copyright (C) 2011 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.tide.cdi;
022
023 import java.lang.annotation.Annotation;
024 import java.lang.reflect.Method;
025 import java.lang.reflect.Type;
026 import java.util.ArrayList;
027 import java.util.HashSet;
028 import java.util.List;
029 import java.util.Map;
030 import java.util.Set;
031
032 import javax.annotation.PreDestroy;
033 import javax.enterprise.context.ContextNotActiveException;
034 import javax.enterprise.context.ConversationScoped;
035 import javax.enterprise.context.RequestScoped;
036 import javax.enterprise.context.SessionScoped;
037 import javax.enterprise.context.spi.CreationalContext;
038 import javax.enterprise.inject.Any;
039 import javax.enterprise.inject.Default;
040 import javax.enterprise.inject.spi.Bean;
041 import javax.enterprise.inject.spi.BeanManager;
042 import javax.enterprise.util.AnnotationLiteral;
043 import javax.inject.Inject;
044 import javax.servlet.http.HttpSession;
045
046 import org.granite.config.GraniteConfig;
047 import org.granite.context.GraniteContext;
048 import org.granite.logging.Logger;
049 import org.granite.messaging.amf.io.util.ClassGetter;
050 import org.granite.messaging.service.ServiceInvocationContext;
051 import org.granite.messaging.webapp.HttpGraniteContext;
052 import org.granite.tide.IInvocationCall;
053 import org.granite.tide.IInvocationResult;
054 import org.granite.tide.TidePersistenceManager;
055 import org.granite.tide.TideServiceContext;
056 import org.granite.tide.annotations.BypassTideMerge;
057 import org.granite.tide.async.AsyncPublisher;
058 import org.granite.tide.cdi.lazy.CDIInitializer;
059 import org.granite.tide.data.DataContext;
060 import org.granite.tide.invocation.ContextEvent;
061 import org.granite.tide.invocation.ContextResult;
062 import org.granite.tide.invocation.ContextUpdate;
063 import org.granite.tide.invocation.InvocationCall;
064 import org.granite.tide.invocation.InvocationResult;
065 import org.granite.util.ClassUtil;
066
067
068 /**
069 * @author William DRAI
070 */
071 @SessionScoped
072 public class CDIServiceContext extends TideServiceContext {
073
074 private static final long serialVersionUID = 1L;
075
076 private static final Logger log = Logger.getLogger(CDIServiceContext.class);
077
078 @SuppressWarnings("serial")
079 private static final AnnotationLiteral<Any> ANY_LITERAL = new AnnotationLiteral<Any>() {};
080 @SuppressWarnings("serial")
081 private static final AnnotationLiteral<Default> DEFAULT_LITERAL = new AnnotationLiteral<Default>() {};
082
083 private @Inject BeanManager manager;
084
085 private UserEvents userEvents;
086 private @Inject TideUserEvents tideUserEvents;
087 private boolean isAsynchronousContext = true;
088 private boolean isFirstCall = false;
089 private boolean isLogin = false;
090 private @Inject CDIInitializer tideEntityInitializer;
091
092
093 /**
094 * Determines the current sessionId for web invocations
095 */
096 @Override
097 public void initCall() {
098 super.initCall();
099
100 if (userEvents != null)
101 return;
102
103 if (getSessionId() != null)
104 userEvents = tideUserEvents.getUserEvents(getSessionId());
105 else {
106 GraniteContext graniteContext = GraniteContext.getCurrentInstance();
107 if (graniteContext instanceof HttpGraniteContext) {
108 HttpSession session = ((HttpGraniteContext)graniteContext).getSession(false);
109 if (session != null)
110 setSessionId(session.getId());
111 isAsynchronousContext = false;
112 }
113 }
114 }
115
116 /**
117 * Initialize current sessionId and event listeners for this context
118 *
119 * @param sessionId current sessionId
120 */
121 @Override
122 public void setSessionId(String sessionId) {
123 super.setSessionId(sessionId);
124 userEvents = tideUserEvents.getUserEvents(sessionId);
125 }
126
127 /**
128 * Clear current session from user events registry
129 */
130 @PreDestroy
131 public void endSession() {
132 if (!isAsynchronousContext && getSessionId() != null)
133 tideUserEvents.unregisterSession(getSessionId());
134 }
135
136
137 @Inject
138 private ResultsEval resultsEval;
139
140 private Map<ContextResult, Boolean> getResultsEval() {
141 return resultsEval.getResultsEval();
142 }
143
144
145 // /**
146 // * Constructs an asynchronous context object
147 // * @return current context
148 // */
149 // public AsyncContext getAsyncContext() {
150 // List<ContextResult> resultsEval = new ArrayList<ContextResult>();
151 // for (ScopeType evalScopeType : EVAL_SCOPE_TYPES)
152 // resultsEval.addAll(getResultsEval(evalScopeType).keySet());
153 //
154 // return new AsyncContext(getSessionId(), resultsEval);
155 // }
156 //
157 // /**
158 // * Restores an asynchronous context
159 // * @param asyncContext saved context
160 // */
161 // public void setAsyncContext(AsyncContext asyncContext) {
162 // AsyncPublisher asyncPublisher = getAsyncPublisher();
163 // if (asyncPublisher != null)
164 // asyncPublisher.initThread();
165 //
166 // Contexts.getSessionContext().set("org.jboss.seam.security.identity", asyncContext.getIdentity());
167 // setSessionId(asyncContext.getSessionId());
168 // for (ContextResult resultEval : asyncContext.getResults()) {
169 // if (resultEval instanceof ScopedContextResult)
170 // getResultsEval(((ScopedContextResult)resultEval).getScope()).put(resultEval, Boolean.FALSE);
171 // else
172 // getResultsEval(ScopeType.UNSPECIFIED).put(resultEval, Boolean.FALSE);
173 // }
174 // }
175
176
177 /**
178 * Implementation of component lookup for CDI service
179 *
180 * @param componentName component name
181 */
182 @Override
183 public Object findComponent(String componentName, Class<?> componentClass) {
184 Bean<?> bean = findBean(componentName, componentClass);
185 if (bean == null)
186 return null;
187 CreationalContext<?> cc = manager.createCreationalContext(bean);
188 return manager.getReference(bean, Object.class, cc);
189 }
190
191 /**
192 * Implementation of component lookup for CDI service
193 *
194 * @param componentName component name
195 */
196 @Override
197 public Set<Class<?>> findComponentClasses(String componentName, Class<?> componentClass) {
198 Bean<?> bean = findBean(componentName, componentClass);
199 return beanClasses(bean);
200 }
201
202 private Bean<?> findBean(String componentName, Class<?> componentClass) {
203 if (componentClass != null) {
204 Set<Bean<?>> beans = manager.getBeans(componentClass, ANY_LITERAL);
205 // If only one match, return match
206 if (beans.size() == 1)
207 return beans.iterator().next();
208 }
209
210 if (componentName != null && !("".equals(componentName))) {
211 // If no previous match, return by name
212 Set<Bean<?>> beans = manager.getBeans(componentName);
213 if (!beans.isEmpty())
214 return beans.iterator().next();
215 }
216
217 if (componentClass != null) {
218 // If more than one match and no named bean, return the Default one
219 Set<Bean<?>> beans = manager.getBeans(componentClass, DEFAULT_LITERAL);
220 if (beans.size() == 1)
221 return beans.iterator().next();
222 }
223
224 return null;
225 }
226
227 private Set<Class<?>> beanClasses(Object bean) {
228 if (bean instanceof Bean<?>) {
229 Set<Class<?>> classes = new HashSet<Class<?>>();
230 for (Type type : ((Bean<?>)bean).getTypes()) {
231 if (type instanceof Class<?>)
232 classes.add((Class<?>)type);
233 }
234 return classes;
235 }
236
237 Set<Class<?>> classes = new HashSet<Class<?>>(1);
238 classes.add(bean.getClass());
239 return classes;
240 }
241
242
243 // public void observeBeginConversation(@Observes org.jboss.webbeans.conversation.) {
244 // Contexts.getEventContext().set("org.granite.tide.conversation.wasCreated", true);
245 // }
246
247
248 /**
249 * Add an event in the current context
250 *
251 * @param event the event
252 */
253 public void processEvent(Object event) {
254 // Add the event to the current invocation
255 TideInvocation tideInvocation = TideInvocation.get();
256 if (tideInvocation == null)
257 return;
258
259 if (userEvents != null) {
260 String sessionId = getSessionId();
261 if (sessionId != null && userEvents.hasEventType(event.getClass()))
262 tideInvocation.addEvent(new ContextEvent(event.getClass().getName(),
263 new Object[] { event, null }));
264 }
265 // else if (Contexts.getSessionContext().isSet("org.granite.seam.login")) {
266 // // Force send of all events raised during login
267 // tideInvocation.addEvent(new ContextEvent(type, params));
268 // }
269 }
270
271
272 /**
273 * Factory for Seam async publisher
274 *
275 * @return servlet context of the current application
276 */
277 @Override
278 protected AsyncPublisher getAsyncPublisher() {
279 return null;
280 }
281
282
283 @Inject
284 private ConversationState conversation;
285
286 /**
287 * Synchronizes server context with data provided by the client
288 *
289 * @param context invocation context
290 * @param c client call
291 * @param componentName name of the component which will be invoked
292 */
293 @Override
294 public void prepareCall(ServiceInvocationContext context, IInvocationCall c, String componentName, Class<?> componentClass) {
295 InvocationCall call = (InvocationCall)c;
296 List<String> listeners = call.getListeners();
297 List<ContextUpdate> updates = call.getUpdates();
298 Object[] results = call.getResults();
299
300 try {
301 if (manager.getContext(RequestScoped.class).isActive() && manager.getContext(SessionScoped.class).isActive() && isFirstCall && isLogin) {
302 // Login tried : force evaluation of existing session context
303 for (Map.Entry<ContextResult, Boolean> me : getResultsEval().entrySet()) {
304 if (me.getKey().getExpression() == null
305 && findBean(me.getKey().getComponentName(), me.getKey().getComponentClass()).getScope().equals(SessionScoped.class))
306 me.setValue(Boolean.TRUE);
307 }
308 isLogin = false;
309 isFirstCall = false;
310 }
311 }
312 catch (ContextNotActiveException e) {
313 // isActive() is not enough !!
314 }
315 try {
316 if (manager.getContext(RequestScoped.class).isActive() && manager.getContext(ConversationScoped.class).isActive()
317 && conversation.isFirstCall()) {
318 // Join conversation : force evaluation of existing conversation context
319 for (Map.Entry<ContextResult, Boolean> me : getResultsEval().entrySet()) {
320 if (me.getKey().getExpression() == null
321 && findBean(me.getKey().getComponentName(), me.getKey().getComponentClass()).getScope().equals(ConversationScoped.class))
322 me.setValue(Boolean.TRUE);
323 }
324 conversation.setFirstCall(false);
325 }
326 }
327 catch (ContextNotActiveException e) {
328 // isActive() is not enough !!
329 }
330
331 String sessionId = getSessionId();
332 if (sessionId != null && listeners != null) {
333 // Registers new event listeners
334 for (String listener : listeners) {
335 try {
336 Class<?> listenerClass = ClassUtil.forName(listener);
337 tideUserEvents.registerEventType(sessionId, listenerClass);
338 }
339 catch (ClassNotFoundException e) {
340 log.error("Could not register event " + listener, e);
341 }
342 }
343
344 if (userEvents == null)
345 userEvents = tideUserEvents.getUserEvents(getSessionId());
346 }
347
348 if (results != null) {
349 Map<ContextResult, Boolean> resultsEval = getResultsEval();
350 for (Object result : results) {
351 ContextResult cr = (ContextResult)result;
352 resultsEval.put(cr, Boolean.TRUE);
353 }
354 }
355
356 try {
357 tideEntityInitializer.restoreLoadedEntities();
358 }
359 catch (ContextNotActiveException e) {
360 // Not in a conversation
361 }
362
363 TideInvocation tideInvocation = TideInvocation.init();
364 tideInvocation.update(updates);
365 }
366
367 /**
368 * Builds the result object for the invocation
369 *
370 * @param context invocation context
371 * @param result result of the method invocation
372 * @param componentName name of the invoked component
373 * @return result object
374 */
375 @Override
376 public IInvocationResult postCall(ServiceInvocationContext context, Object result, String componentName, Class<?> componentClass) {
377 TideInvocation tideInvocation = TideInvocation.get();
378 int scope = 3;
379 boolean restrict = false;
380
381 List<ContextUpdate> results = new ArrayList<ContextUpdate>(tideInvocation.getResults());
382 DataContext dataContext = DataContext.get();
383 Set<Object[]> dataUpdates = dataContext != null ? dataContext.getDataUpdates() : null;
384 Object[][] updates = null;
385 if (dataUpdates != null && !dataUpdates.isEmpty())
386 updates = dataUpdates.toArray(new Object[dataUpdates.size()][]);
387
388 Bean<?> bean = null;
389 if (componentName != null || componentClass != null) {
390 // Determines scope of component
391 bean = findBean(componentName, componentClass);
392 if (bean.getScope() == RequestScoped.class)
393 scope = 3;
394 else if (bean.getScope() == ConversationScoped.class)
395 scope = 2;
396 else if (bean.getScope() == SessionScoped.class)
397 scope = 1;
398
399 try {
400 if (manager.getContext(RequestScoped.class).get(bean) != null)
401 scope = 3;
402 else if (manager.getContext(ConversationScoped.class).get(bean) != null)
403 scope = 2;
404 else if (manager.getContext(SessionScoped.class).get(bean) != null)
405 scope = 1;
406 }
407 catch (ContextNotActiveException e) {
408 scope = 3;
409 }
410 }
411
412 InvocationResult res = new InvocationResult(result, results);
413 res.setScope(scope);
414 res.setRestrict(restrict);
415 if (bean != null) {
416 if (bean.getBeanClass().isAnnotationPresent(BypassTideMerge.class))
417 res.setMerge(false);
418 else {
419 try {
420 if (context != null) {
421 Method m = bean.getBeanClass().getMethod(context.getMethod().getName(), context.getMethod().getParameterTypes());
422 if (m.isAnnotationPresent(BypassTideMerge.class))
423 res.setMerge(false);
424 }
425 }
426 catch (Exception e) {
427 log.warn("Could not find bean method", e);
428 }
429 }
430 }
431
432 res.setUpdates(updates);
433
434 // Adds events in result object
435 res.setEvents(tideInvocation.getEvents());
436
437 try {
438 // Save current set of entities loaded in a conversation scoped component to handle case of extended PM
439 tideEntityInitializer.saveLoadedEntities();
440 }
441 catch (ContextNotActiveException e) {
442 // Not in a conversation
443 }
444
445 // Clean thread
446 TideInvocation.remove();
447
448 return res;
449 }
450
451 /**
452 * Intercepts a fault on the invocation
453 *
454 * @param context invocation context
455 * @param t exception thrown
456 * @param componentName name of the invoked component
457 */
458 @Override
459 public void postCallFault(ServiceInvocationContext context, Throwable t, String componentName, Class<?> componentClass) {
460 // Clean thread: very important to avoid phantom evaluations after exceptions
461 TideInvocation.remove();
462 }
463
464
465 public void addResultEval(ContextResult result) {
466 getResultsEval().put(result, Boolean.TRUE);
467 }
468
469
470 /**
471 * Evaluate updates in current server context
472 *
473 * @param updates list of updates
474 * @param target the target instance
475 */
476 @SuppressWarnings("serial")
477 public void restoreContext(List<ContextUpdate> updates, Object target) {
478 if (updates == null)
479 return;
480
481 GraniteConfig config = GraniteContext.getCurrentInstance().getGraniteConfig();
482
483 // Restore context
484 for (ContextUpdate update : updates) {
485
486 log.debug("Before invocation: evaluating expression #0.#1", update.getComponentName(), update.getExpression());
487
488 Set<Bean<?>> beans = manager.getBeans(update.getValue().getClass(), new AnnotationLiteral<Any>() {});
489 if (beans.isEmpty() && update.getComponentName() != null)
490 beans = manager.getBeans(update.getComponentName());
491
492 if (!beans.isEmpty()) {
493 Bean<?> sourceBean = beans.iterator().next();
494
495 Object previous = manager.getReference(sourceBean, Object.class, manager.createCreationalContext(sourceBean));
496 boolean disabled = previous != null && sourceBean != null
497 && config.isComponentTideDisabled(sourceBean.getName(), beanClasses(sourceBean), previous);
498 if (!disabled)
499 mergeExternal(update.getValue(), previous);
500 }
501 }
502 }
503
504
505 /**
506 * Evaluate results from context
507 *
508 * @param target the target instance
509 * @param nothing used by initializer to avoid interactions with context sync
510 *
511 * @return list of updates to send back to the client
512 */
513 public List<ContextUpdate> evaluateResults(Object target, boolean nothing) {
514
515 List<ContextUpdate> resultsMap = new ArrayList<ContextUpdate>();
516
517 if (nothing)
518 return resultsMap;
519
520 GraniteConfig config = GraniteContext.getCurrentInstance().getGraniteConfig();
521 ClassGetter classGetter = GraniteContext.getCurrentInstance().getGraniteConfig().getClassGetter();
522
523 for (Map.Entry<ContextResult, Boolean> me : getResultsEval().entrySet()) {
524 if (!me.getValue())
525 continue;
526
527 ContextResult res = me.getKey();
528
529 Class<?> componentClass = res.getComponentClass();
530 Bean<?> targetBean = findBean(res.getComponentName(), componentClass);
531 String targetComponentName = targetBean.getName();
532
533 boolean add = true;
534 Class<? extends Annotation> scopeType = targetBean.getScope();
535 Boolean restrict = res.getRestrict();
536
537 Object value = res instanceof ScopedContextResult
538 ? ((ScopedContextResult)res).getValue()
539 : manager.getReference(targetBean, Object.class, null);
540
541 if (value != null && config.isComponentTideDisabled(targetComponentName, beanClasses(targetBean), value))
542 add = false;
543
544 if (add) {
545 getResultsEval().put(res, false);
546
547 if (value != null && classGetter != null) {
548 classGetter.initialize(null, null, value);
549
550 int scope = 3;
551 if (scopeType == ConversationScoped.class)
552 scope = 2;
553 else if (scopeType == SessionScoped.class)
554 scope = 1;
555
556 resultsMap.add(new ContextUpdate(res.getComponentName(), null, value, scope, Boolean.TRUE.equals(restrict)));
557 add = false;
558 }
559 }
560
561 me.setValue(Boolean.FALSE);
562 }
563
564 return resultsMap;
565 }
566
567 @Inject
568 private CDIInitializer initializer;
569
570 @Override
571 protected TidePersistenceManager getTidePersistenceManager(boolean create) {
572 return initializer.getPersistenceManager();
573 }
574
575
576 @Override
577 protected boolean equals(Object obj1, Object obj2) {
578 if (super.equals(obj1, obj2))
579 return true;
580
581 return (obj1 != null && obj2 != null
582 && (obj1.getClass().isAnnotationPresent(TideBean.class) || obj2.getClass().isAnnotationPresent(TideBean.class)));
583 }
584
585
586 // /**
587 // * Implementations of intercepted asynchronous calls
588 // * Send asynchronous event to client
589 // * @param asyncContext current context (session id)
590 // * @param targetComponentName target component name
591 // * @param methodName method name
592 // * @param paramTypes method argument types
593 // * @param params argument values
594 // * @return result
595 // */
596 // public Object invokeAsynchronous(AsyncContext asyncContext, String targetComponentName, String methodName, Class<?>[] paramTypes, Object[] params) {
597 // setAsyncContext(asyncContext);
598 //
599 // // Just another ugly hack: the Seam interceptor has set this variable and we don't want it
600 // Contexts.getEventContext().remove("org.jboss.seam.async.AsynchronousIntercepter.REENTRANT");
601 //
602 // Component component = TideInit.lookupComponent(targetComponentName);
603 //
604 // // Forces evaluation of all results if they are related to the called component
605 // for (Map.Entry<ContextResult, Boolean> me : getResultsEval(component.getScope()).entrySet()) {
606 // if (me.getKey().getComponentName().equals(targetComponentName))
607 // me.setValue(Boolean.TRUE);
608 // }
609 //
610 // Object target = Component.getInstance(targetComponentName);
611 //
612 // Method method;
613 // try {
614 // method = target.getClass().getMethod(methodName, paramTypes);
615 // }
616 // catch (NoSuchMethodException nsme) {
617 // throw new IllegalStateException(nsme);
618 // }
619 //
620 // Object result = Reflections.invokeAndWrap(method, target, params);
621 //
622 // sendEvent(targetComponentName);
623 //
624 // return result;
625 // }
626 //
627 //
628 // /**
629 // * Search for a named attribute in all contexts, in the
630 // * following order: method, event, page, conversation,
631 // * session, business process, application.
632 // *
633 // * @return the first component found, or null
634 // */
635 // public static Object[] lookupInStatefulContexts(String name, ScopeType scope) {
636 // if ((ScopeType.UNSPECIFIED.equals(scope) || ScopeType.METHOD.equals(scope)) && Contexts.isMethodContextActive()) {
637 // Object result = Contexts.getMethodContext().get(name);
638 // if (result != null)
639 // return new Object[] { result, Contexts.getMethodContext().getType() };
640 // }
641 //
642 // if ((ScopeType.UNSPECIFIED.equals(scope) || ScopeType.EVENT.equals(scope)) && Contexts.isEventContextActive()) {
643 // Object result = Contexts.getEventContext().get(name);
644 // if (result != null)
645 // return new Object[] { result, Contexts.getEventContext().getType() };
646 // }
647 //
648 // if ((ScopeType.UNSPECIFIED.equals(scope) || ScopeType.PAGE.equals(scope)) && Contexts.isPageContextActive()) {
649 // Object result = Contexts.getPageContext().get(name);
650 // if (result != null)
651 // return new Object[] { result, Contexts.getPageContext().getType() };
652 // }
653 //
654 // if ((ScopeType.UNSPECIFIED.equals(scope) || ScopeType.CONVERSATION.equals(scope)) && Contexts.isConversationContextActive()) {
655 // Object result = Contexts.getConversationContext().get(name);
656 // if (result != null)
657 // return new Object[] { result, Contexts.getConversationContext().getType() };
658 // }
659 //
660 // if ((ScopeType.UNSPECIFIED.equals(scope) || ScopeType.SESSION.equals(scope)) && Contexts.isSessionContextActive()) {
661 // Object result = Contexts.getSessionContext().get(name);
662 // if (result != null)
663 // return new Object[] { result, Contexts.getSessionContext().getType() };
664 // }
665 //
666 // if ((ScopeType.UNSPECIFIED.equals(scope) || ScopeType.BUSINESS_PROCESS.equals(scope)) && Contexts.isBusinessProcessContextActive()) {
667 // Object result = Contexts.getBusinessProcessContext().get(name);
668 // if (result != null)
669 // return new Object[] { result, Contexts.getBusinessProcessContext().getType() };
670 // }
671 //
672 // if ((ScopeType.UNSPECIFIED.equals(scope) || ScopeType.APPLICATION.equals(scope)) && Contexts.isApplicationContextActive()) {
673 // Object result = Contexts.getApplicationContext().get(name);
674 // if (result != null)
675 // return new Object[] { result, Contexts.getApplicationContext().getType() };
676 // }
677 //
678 // return ScopeType.UNSPECIFIED.equals(scope) ? null : new Object[] { null, scope };
679 // }
680 }