/** * GRANITE DATA SERVICES * Copyright (C) 2006-2015 GRANITE DATA SERVICES S.A.S. * * This file is part of the Granite Data Services Platform. * * Granite Data Services is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * Granite Data Services is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser * General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, * USA, or see <http://www.gnu.org/licenses/>. */ package org.granite.tide.cdi; import java.lang.annotation.Annotation; import java.lang.reflect.Method; import java.lang.reflect.Type; import java.math.BigDecimal; import java.math.BigInteger; import java.util.ArrayList; import java.util.Arrays; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import javax.annotation.PreDestroy; import javax.enterprise.context.ContextNotActiveException; import javax.enterprise.context.ConversationScoped; import javax.enterprise.context.RequestScoped; import javax.enterprise.context.SessionScoped; import javax.enterprise.context.spi.CreationalContext; import javax.enterprise.inject.Any; import javax.enterprise.inject.Default; import javax.enterprise.inject.IllegalProductException; import javax.enterprise.inject.spi.Bean; import javax.enterprise.inject.spi.BeanManager; import javax.enterprise.util.AnnotationLiteral; import javax.inject.Inject; import javax.persistence.Entity; import javax.servlet.http.HttpSession; import org.granite.config.ConvertersConfig; import org.granite.config.GraniteConfig; import org.granite.context.GraniteContext; import org.granite.logging.Logger; import org.granite.messaging.amf.io.util.ClassGetter; import org.granite.messaging.service.ServiceException; import org.granite.messaging.service.ServiceInvocationContext; import org.granite.messaging.webapp.HttpGraniteContext; import org.granite.tide.IInvocationCall; import org.granite.tide.IInvocationResult; import org.granite.tide.TidePersistenceManager; import org.granite.tide.TideServiceContext; import org.granite.tide.annotations.BypassTideMerge; import org.granite.tide.async.AsyncPublisher; import org.granite.tide.cdi.lazy.CDIInitializer; import org.granite.tide.data.DataContext; import org.granite.tide.data.DataUpdatePostprocessor; import org.granite.tide.data.DisableRemoteUpdates; import org.granite.tide.invocation.ContextEvent; import org.granite.tide.invocation.ContextResult; import org.granite.tide.invocation.ContextUpdate; import org.granite.tide.invocation.InvocationCall; import org.granite.tide.invocation.InvocationResult; import org.granite.util.TypeUtil; import org.granite.util.Reflections; import org.jboss.interceptor.util.proxy.TargetInstanceProxy; /** * @author William DRAI */ @SessionScoped public class CDIServiceContext extends TideServiceContext { private static final long serialVersionUID = 1L; private static final Logger log = Logger.getLogger(CDIServiceContext.class); @SuppressWarnings("serial") private static final AnnotationLiteral<Any> ANY_LITERAL = new AnnotationLiteral<Any>() {}; @SuppressWarnings("serial") private static final AnnotationLiteral<Default> DEFAULT_LITERAL = new AnnotationLiteral<Default>() {}; private @Inject BeanManager manager; private @Inject TideInstrumentedBeans instrumentedBeans; private UserEvents userEvents; private @Inject TideUserEvents tideUserEvents; private boolean isAsynchronousContext = true; private boolean isFirstCall = false; private boolean isLogin = false; private @Inject CDIInitializer tideEntityInitializer; /** * Determines the current sessionId for web invocations */ @Override public void initCall() { super.initCall(); if (userEvents != null) return; if (getSessionId() != null) userEvents = tideUserEvents.getUserEvents(getSessionId()); else { GraniteContext graniteContext = GraniteContext.getCurrentInstance(); if (graniteContext instanceof HttpGraniteContext) { HttpSession session = ((HttpGraniteContext)graniteContext).getSession(false); if (session != null) setSessionId(session.getId()); isAsynchronousContext = false; } } } /** * Initialize current sessionId and event listeners for this context * * @param sessionId current sessionId */ @Override public void setSessionId(String sessionId) { super.setSessionId(sessionId); userEvents = tideUserEvents.getUserEvents(sessionId); } /** * Clear current session from user events registry */ @PreDestroy public void endSession() { if (!isAsynchronousContext && getSessionId() != null) tideUserEvents.unregisterSession(getSessionId()); } public void setLogin(boolean isLogin) { this.isLogin = isLogin; } @Inject private ResultsEval resultsEval; public Map<ContextResult, Boolean> getResultsEval() { return resultsEval.getResultsEval(); } // /** // * Constructs an asynchronous context object // * @return current context // */ // public AsyncContext getAsyncContext() { // List<ContextResult> resultsEval = new ArrayList<ContextResult>(); // for (ScopeType evalScopeType : EVAL_SCOPE_TYPES) // resultsEval.addAll(getResultsEval(evalScopeType).keySet()); // // return new AsyncContext(getSessionId(), resultsEval); // } // // /** // * Restores an asynchronous context // * @param asyncContext saved context // */ // public void setAsyncContext(AsyncContext asyncContext) { // AsyncPublisher asyncPublisher = getAsyncPublisher(); // if (asyncPublisher != null) // asyncPublisher.initThread(); // // Contexts.getSessionContext().set("org.jboss.seam.security.identity", asyncContext.getIdentity()); // setSessionId(asyncContext.getSessionId()); // for (ContextResult resultEval : asyncContext.getResults()) { // if (resultEval instanceof ScopedContextResult) // getResultsEval(((ScopedContextResult)resultEval).getScope()).put(resultEval, Boolean.FALSE); // else // getResultsEval(ScopeType.UNSPECIFIED).put(resultEval, Boolean.FALSE); // } // } /** * Implementation of component lookup for CDI service * * @param componentName component name */ @Override public Object findComponent(String componentName, Class<?> componentClass, String methodName) { Bean<?> bean = findBean(componentName, componentClass); if (bean == null) return null; CreationalContext<?> cc = manager.createCreationalContext(bean); return manager.getReference(bean, Object.class, cc); } /** * Implementation of component lookup for CDI service * * @param componentName component name */ @Override public Set<Class<?>> findComponentClasses(String componentName, Class<?> componentClass, String methodName) { Bean<?> bean = findBean(componentName, componentClass); if (bean == null) return null; return beanClasses(bean); } private Bean<?> findBean(String componentName, Class<?> componentClass) { if (componentClass != null) { Set<Bean<?>> beans = manager.getBeans(componentClass, ANY_LITERAL); // If only one match, return match if (beans.size() == 1) return beans.iterator().next(); } if (componentName != null && !("".equals(componentName))) { // If no previous match, return by name Set<Bean<?>> beans = manager.getBeans(componentName); if (!beans.isEmpty()) return beans.iterator().next(); } if (componentClass != null) { // If more than one match and no named bean, return the Default one Set<Bean<?>> beans = manager.getBeans(componentClass, DEFAULT_LITERAL); if (beans.size() == 1) return beans.iterator().next(); } return null; } private Set<Class<?>> beanClasses(Object bean) { if (bean instanceof Bean<?>) { Set<Class<?>> classes = new HashSet<Class<?>>(); for (Type type : ((Bean<?>)bean).getTypes()) { if (type instanceof Class<?>) classes.add((Class<?>)type); } return classes; } Set<Class<?>> classes = new HashSet<Class<?>>(1); classes.add(bean.getClass()); return classes; } // public void observeBeginConversation(@Observes org.jboss.webbeans.conversation.) { // Contexts.getEventContext().set("org.granite.tide.conversation.wasCreated", true); // } /** * Add an event in the current context * * @param event the event */ public void processEvent(Object event) { // Add the event to the current invocation TideInvocation tideInvocation = TideInvocation.get(); if (tideInvocation == null) return; if (userEvents != null) { String sessionId = getSessionId(); if (sessionId != null && userEvents.hasEventType(event.getClass())) tideInvocation.addEvent(new ContextEvent(event.getClass().getName(), new Object[] { event, null })); } // else if (Contexts.getSessionContext().isSet("org.granite.seam.login")) { // // Force send of all events raised during login // tideInvocation.addEvent(new ContextEvent(type, params)); // } } /** * Factory for Seam async publisher * * @return servlet context of the current application */ @Override protected AsyncPublisher getAsyncPublisher() { return null; } @Inject private ConversationState conversation; /** * Synchronizes server context with data provided by the client * * @param context invocation context * @param c client call * @param componentName name of the component which will be invoked */ @Override public void prepareCall(ServiceInvocationContext context, IInvocationCall c, String componentName, Class<?> componentClass) { InvocationCall call = (InvocationCall)c; List<String> listeners = call.getListeners(); List<ContextUpdate> updates = call.getUpdates(); Object[] results = call.getResults(); try { if (manager.getContext(RequestScoped.class).isActive() && manager.getContext(SessionScoped.class).isActive() && isFirstCall && isLogin) { // Login tried : force evaluation of existing session context for (Map.Entry<ContextResult, Boolean> me : getResultsEval().entrySet()) { if (me.getKey().getExpression() == null && findBean(me.getKey().getComponentName(), me.getKey().getComponentClass()).getScope().equals(SessionScoped.class)) me.setValue(Boolean.TRUE); } isLogin = false; isFirstCall = false; } } catch (ContextNotActiveException e) { // isActive() is not enough !! } try { if (manager.getContext(RequestScoped.class).isActive() && manager.getContext(ConversationScoped.class).isActive() && conversation.isFirstCall()) { // Join conversation : force evaluation of existing conversation context for (Map.Entry<ContextResult, Boolean> me : getResultsEval().entrySet()) { if (me.getKey().getExpression() == null && findBean(me.getKey().getComponentName(), me.getKey().getComponentClass()).getScope().equals(ConversationScoped.class)) me.setValue(Boolean.TRUE); } conversation.setFirstCall(false); } } catch (ContextNotActiveException e) { // isActive() is not enough !! } String sessionId = getSessionId(); if (sessionId != null && listeners != null) { // Registers new event listeners for (String listener : listeners) { try { Class<?> listenerClass = TypeUtil.forName(listener); tideUserEvents.registerEventType(sessionId, listenerClass); } catch (ClassNotFoundException e) { log.error("Could not register event " + listener, e); } } if (userEvents == null) userEvents = tideUserEvents.getUserEvents(getSessionId()); } boolean instrumented = false; Bean<?> bean = findBean(componentName, componentClass); if (bean != null) instrumented = instrumentedBeans.getBean(bean.getBeanClass()) != null; if (results != null) { Map<ContextResult, Boolean> resultsEval = getResultsEval(); for (Object result : results) { ContextResult cr = (ContextResult)result; resultsEval.put(cr, Boolean.TRUE); } } try { tideEntityInitializer.restoreLoadedEntities(); } catch (ContextNotActiveException e) { // Not in a conversation } // Initialize an empty data context DataContext.init(); DataUpdatePostprocessor dataUpdatePostprocessor = (DataUpdatePostprocessor)findComponent(null, DataUpdatePostprocessor.class, null); if (dataUpdatePostprocessor != null) DataContext.get().setDataUpdatePostprocessor(dataUpdatePostprocessor); TideInvocation tideInvocation = TideInvocation.init(); tideInvocation.update(updates); if (!instrumented) { // If no interception enabled, force the update of the context for the current component // In other cases it will be done by the interceptor restoreContext(updates, null); tideInvocation.updated(); } } /** * Builds the result object for the invocation * * @param context invocation context * @param result result of the method invocation * @param componentName name of the invoked component * @return result object */ @Override public IInvocationResult postCall(ServiceInvocationContext context, Object result, String componentName, Class<?> componentClass) { TideInvocation tideInvocation = TideInvocation.get(); int scope = 3; boolean restrict = false; List<ContextUpdate> results = null; if (!tideInvocation.isEvaluated()) { // Do evaluation now if the interceptor has not been called results = evaluateResults(null, false); } else results = tideInvocation.getResults(); Bean<?> bean = null; if (componentName != null || componentClass != null) { // Determines scope of component bean = findBean(componentName, componentClass); if (bean.getScope() == RequestScoped.class) scope = 3; else if (bean.getScope() == ConversationScoped.class) scope = 2; else if (bean.getScope() == SessionScoped.class) scope = 1; try { if (manager.getContext(RequestScoped.class).get(bean) != null) scope = 3; else if (manager.getContext(ConversationScoped.class).get(bean) != null) scope = 2; else if (manager.getContext(SessionScoped.class).get(bean) != null) scope = 1; } catch (ContextNotActiveException e) { scope = 3; } } InvocationResult ires = new InvocationResult(result, results); ires.setScope(scope); ires.setRestrict(restrict); if (componentName != null || componentClass != null) { Set<Class<?>> componentClasses = findComponentClasses(componentName, componentClass, null); if (isBeanAnnotationPresent(componentClasses, context.getMethod().getName(), context.getMethod().getParameterTypes(), BypassTideMerge.class)) ires.setMerge(false); } boolean enableUpdates = true; if (componentName != null || componentClass != null) { Set<Class<?>> componentClasses = findComponentClasses(componentName, componentClass, null); if (isBeanAnnotationPresent(componentClasses, context.getMethod().getName(), context.getMethod().getParameterTypes(), DisableRemoteUpdates.class)) enableUpdates = false; } if (enableUpdates) { DataContext dataContext = DataContext.get(); Object[][] updates = dataContext != null ? dataContext.getUpdates() : null; ires.setUpdates(updates); } // Adds events in result object ires.setEvents(tideInvocation.getEvents()); try { // Save current set of entities loaded in a conversation scoped component to handle case of extended PM tideEntityInitializer.saveLoadedEntities(); } catch (ContextNotActiveException e) { // Not in a conversation } // Clean thread TideInvocation.remove(); return ires; } /** * Intercepts a fault on the invocation * * @param context invocation context * @param t exception thrown * @param componentName name of the invoked component */ @Override public void postCallFault(ServiceInvocationContext context, Throwable t, String componentName, Class<?> componentClass) { // Clean thread: very important to avoid phantom evaluations after exceptions TideInvocation.remove(); } public void addResultEval(ContextResult result) { getResultsEval().put(result, Boolean.TRUE); } private static Object unproxy(Object obj) { try { // Works only with Weld !! if (obj instanceof TargetInstanceProxy<?>) { try { return ((TargetInstanceProxy<?>)obj).getTargetInstance(); } catch (IllegalProductException e) { return null; } } return obj; } catch (Throwable t) { // Ignore, stateful support is supposed to work only with Weld return obj; } } /** * Evaluate updates in current server context * * @param updates list of updates * @param target the target instance */ public void restoreContext(List<ContextUpdate> updates, Object target) { if (updates == null) return; try { TideInvocation.get().lock(); GraniteConfig config = GraniteContext.getCurrentInstance().getGraniteConfig(); // Restore context for (ContextUpdate update : updates) { log.debug("Before invocation: evaluating expression #0(#1).#2", update.getComponentName(), update.getComponentClassName(), update.getExpression()); Class<?> componentClass = update.getComponentClass(); Bean<?> sourceBean = findBean(update.getComponentName(), componentClass); Object previous = null; if (update.getExpression() != null) { String[] path = update.getExpression().split("\\."); Object instance = manager.getReference(sourceBean, Object.class, manager.createCreationalContext(sourceBean)); boolean disabled = instance != null && config.isComponentTideDisabled(sourceBean.getName(), beanClasses(sourceBean), instance); if (!disabled) { instance = unproxy(instance); Object bean = instance; Object value = instance; if (update.getValue() != null) { boolean getPrevious = true; if (update.getValue().getClass().isPrimitive() || isImmutable(update.getValue())) { getPrevious = false; } else if (update.getValue().getClass().getAnnotation(Entity.class) != null) { org.granite.util.Entity entity = new org.granite.util.Entity(update.getValue()); if (entity.getIdentifier() == null) getPrevious = false; } if (getPrevious) { try { for (int i = 0; i < path.length; i++) { if (value == null) break; // Use modified Reflections for getter because of a bug in Seam 2.0.0 Method getter = org.granite.util.Reflections.getGetterMethod(value.getClass(), path[i]); value = Reflections.invoke(getter, value); if (i < path.length-1) bean = value; } } catch (IllegalArgumentException e) { // No getter found to retrieve current value log.warn(e, "Partial merge only"); value = null; } catch (Exception e) { throw new ServiceException("Could not get property: " + update.toString(), e); } previous = value; } } // Set new value try { if (bean != null) { Method setter = Reflections.getSetterMethod(bean.getClass(), path[path.length-1]); Type type = setter.getParameterTypes()[0]; value = ((ConvertersConfig)GraniteContext.getCurrentInstance().getGraniteConfig()).getConverters().convert(update.getValue(), type); // Merge entities into current persistent context if needed value = mergeExternal(value, previous); Reflections.invoke(setter, bean, value); } } catch (Exception e) { throw new ServiceException("Could not restore property: " + update.toString(), e); } } } else { previous = manager.getReference(sourceBean, Object.class, manager.createCreationalContext(sourceBean)); boolean disabled = previous != null && config.isComponentTideDisabled(sourceBean.getName(), beanClasses(sourceBean), previous); if (!disabled) { previous = unproxy(previous); // Merge context variable mergeExternal(update.getValue(), previous); } } } } finally { TideInvocation.get().unlock(); } } /** * Evaluate results from context * * @param target the target instance * @param nothing used by initializer to avoid interactions with context sync * * @return list of updates to send back to the client */ public List<ContextUpdate> evaluateResults(Object target, boolean nothing) { List<ContextUpdate> resultsMap = new ArrayList<ContextUpdate>(); if (nothing) return resultsMap; try { TideInvocation.get().lock(); GraniteConfig config = GraniteContext.getCurrentInstance().getGraniteConfig(); ClassGetter classGetter = ((GraniteConfig)GraniteContext.getCurrentInstance().getGraniteConfig()).getClassGetter(); for (Map.Entry<ContextResult, Boolean> me : getResultsEval().entrySet()) { if (!me.getValue()) continue; ContextResult res = me.getKey(); Class<?> componentClass = res.getComponentClass(); Bean<?> targetBean = findBean(res.getComponentName(), componentClass); if (targetBean == null) { log.warn("Target bean " + res.getComponentName() + " of class " + componentClass + " not found"); continue; } String targetComponentName = targetBean.getName(); boolean add = true; Class<? extends Annotation> scopeType = targetBean.getScope(); Boolean restrict = res.getRestrict(); Object value = res instanceof ScopedContextResult ? ((ScopedContextResult)res).getValue() : manager.getReference(targetBean, Object.class, manager.createCreationalContext(targetBean)); if (value != null && config.isComponentTideDisabled(targetComponentName, beanClasses(targetBean), value)) add = false; if (add) { getResultsEval().put(res, false); String[] path = res.getExpression() != null ? res.getExpression().split("\\.") : new String[0]; value = unproxy(value); if (value != null) { try { for (int i = 0; i < path.length; i++) { if (value == null) break; try { Method getter = Reflections.getGetterMethod(value.getClass(), path[i]); value = Reflections.invoke(getter, value); } catch (IllegalArgumentException e) { // GDS-566 add = false; } } } catch (Exception e) { throw new ServiceException("Could not evaluate expression " + res.toString(), e); } } if (add && value != null && classGetter != null) { classGetter.initialize(null, null, value); int scope = 3; if (scopeType == ConversationScoped.class) scope = 2; else if (scopeType == SessionScoped.class) scope = 1; ContextUpdate cu = new ContextUpdate(res.getComponentName(), res.getExpression(), value, scope, Boolean.TRUE.equals(restrict)); cu.setComponentClassName(res.getComponentClassName()); resultsMap.add(cu); add = false; } } me.setValue(Boolean.FALSE); } return resultsMap; } finally { TideInvocation.get().unlock(); } } @Inject private CDIInitializer initializer; @Override protected TidePersistenceManager getTidePersistenceManager(boolean create) { return initializer.getPersistenceManager(); } @Override protected boolean equals(Object obj1, Object obj2) { if (super.equals(obj1, obj2)) return true; return (obj1 != null && obj2 != null && (obj1.getClass().isAnnotationPresent(TideBean.class) || obj2.getClass().isAnnotationPresent(TideBean.class))); } private static final Set<Class<?>> KNOWN_IMMUTABLES = new HashSet<Class<?>>(Arrays.asList( String.class, Byte.class, Short.class, Integer.class, Long.class, Float.class, Double.class, Boolean.class, BigInteger.class, BigDecimal.class )); public static boolean isImmutable(Object o) { return KNOWN_IMMUTABLES.contains(o.getClass()) || Enum.class.isInstance(o); } // /** // * Implementations of intercepted asynchronous calls // * Send asynchronous event to client // * @param asyncContext current context (session id) // * @param targetComponentName target component name // * @param methodName method name // * @param paramTypes method argument types // * @param params argument values // * @return result // */ // public Object invokeAsynchronous(AsyncContext asyncContext, String targetComponentName, String methodName, Class<?>[] paramTypes, Object[] params) { // setAsyncContext(asyncContext); // // // Just another ugly hack: the Seam interceptor has set this variable and we don't want it // Contexts.getEventContext().remove("org.jboss.seam.async.AsynchronousIntercepter.REENTRANT"); // // Component component = TideInit.lookupComponent(targetComponentName); // // // Forces evaluation of all results if they are related to the called component // for (Map.Entry<ContextResult, Boolean> me : getResultsEval(component.getScope()).entrySet()) { // if (me.getKey().getComponentName().equals(targetComponentName)) // me.setValue(Boolean.TRUE); // } // // Object target = Component.getInstance(targetComponentName); // // Method method; // try { // method = target.getClass().getMethod(methodName, paramTypes); // } // catch (NoSuchMethodException nsme) { // throw new IllegalStateException(nsme); // } // // Object result = Reflections.invokeAndWrap(method, target, params); // // sendEvent(targetComponentName); // // return result; // } // // // /** // * Search for a named attribute in all contexts, in the // * following order: method, event, page, conversation, // * session, business process, application. // * // * @return the first component found, or null // */ // public static Object[] lookupInStatefulContexts(String name, ScopeType scope) { // if ((ScopeType.UNSPECIFIED.equals(scope) || ScopeType.METHOD.equals(scope)) && Contexts.isMethodContextActive()) { // Object result = Contexts.getMethodContext().get(name); // if (result != null) // return new Object[] { result, Contexts.getMethodContext().getType() }; // } // // if ((ScopeType.UNSPECIFIED.equals(scope) || ScopeType.EVENT.equals(scope)) && Contexts.isEventContextActive()) { // Object result = Contexts.getEventContext().get(name); // if (result != null) // return new Object[] { result, Contexts.getEventContext().getType() }; // } // // if ((ScopeType.UNSPECIFIED.equals(scope) || ScopeType.PAGE.equals(scope)) && Contexts.isPageContextActive()) { // Object result = Contexts.getPageContext().get(name); // if (result != null) // return new Object[] { result, Contexts.getPageContext().getType() }; // } // // if ((ScopeType.UNSPECIFIED.equals(scope) || ScopeType.CONVERSATION.equals(scope)) && Contexts.isConversationContextActive()) { // Object result = Contexts.getConversationContext().get(name); // if (result != null) // return new Object[] { result, Contexts.getConversationContext().getType() }; // } // // if ((ScopeType.UNSPECIFIED.equals(scope) || ScopeType.SESSION.equals(scope)) && Contexts.isSessionContextActive()) { // Object result = Contexts.getSessionContext().get(name); // if (result != null) // return new Object[] { result, Contexts.getSessionContext().getType() }; // } // // if ((ScopeType.UNSPECIFIED.equals(scope) || ScopeType.BUSINESS_PROCESS.equals(scope)) && Contexts.isBusinessProcessContextActive()) { // Object result = Contexts.getBusinessProcessContext().get(name); // if (result != null) // return new Object[] { result, Contexts.getBusinessProcessContext().getType() }; // } // // if ((ScopeType.UNSPECIFIED.equals(scope) || ScopeType.APPLICATION.equals(scope)) && Contexts.isApplicationContextActive()) { // Object result = Contexts.getApplicationContext().get(name); // if (result != null) // return new Object[] { result, Contexts.getApplicationContext().getType() }; // } // // return ScopeType.UNSPECIFIED.equals(scope) ? null : new Object[] { null, scope }; // } }