001    /*
002      GRANITE DATA SERVICES
003      Copyright (C) 2007-2010 ADEQUATE SYSTEMS SARL
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.seam;
022    
023    import java.lang.reflect.Field;
024    import java.lang.reflect.Method;
025    import java.lang.reflect.Type;
026    import java.util.ArrayList;
027    import java.util.HashMap;
028    import java.util.HashSet;
029    import java.util.List;
030    import java.util.Map;
031    import java.util.Set;
032    
033    import javax.persistence.Entity;
034    import javax.servlet.http.HttpSession;
035    
036    import org.granite.config.GraniteConfig;
037    import org.granite.context.GraniteContext;
038    import org.granite.messaging.amf.io.util.ClassGetter;
039    import org.granite.messaging.service.ServiceException;
040    import org.granite.messaging.service.ServiceInvocationContext;
041    import org.granite.messaging.webapp.HttpGraniteContext;
042    import org.granite.tide.IInvocationCall;
043    import org.granite.tide.IInvocationResult;
044    import org.granite.tide.TidePersistenceManager;
045    import org.granite.tide.TideServiceContext;
046    import org.granite.tide.TideStatusMessages;
047    import org.granite.tide.annotations.BypassTideMerge;
048    import org.granite.tide.async.AsyncPublisher;
049    import org.granite.tide.data.DataContext;
050    import org.granite.tide.data.DataMergeContext;
051    import org.granite.tide.invocation.ContextEvent;
052    import org.granite.tide.invocation.ContextResult;
053    import org.granite.tide.invocation.ContextUpdate;
054    import org.granite.tide.invocation.InvocationCall;
055    import org.granite.tide.invocation.InvocationResult;
056    import org.granite.tide.seam.async.AsyncContext;
057    import org.granite.tide.seam.lazy.SeamInitializer;
058    import org.jboss.seam.Component;
059    import org.jboss.seam.ScopeType;
060    import org.jboss.seam.annotations.Destroy;
061    import org.jboss.seam.annotations.Factory;
062    import org.jboss.seam.annotations.Logger;
063    import org.jboss.seam.annotations.Observer;
064    import org.jboss.seam.annotations.datamodel.DataModelSelection;
065    import org.jboss.seam.annotations.security.Restrict;
066    import org.jboss.seam.contexts.Context;
067    import org.jboss.seam.contexts.Contexts;
068    import org.jboss.seam.core.Conversation;
069    import org.jboss.seam.core.Init.FactoryExpression;
070    import org.jboss.seam.core.Init.FactoryMethod;
071    import org.jboss.seam.framework.Home;
072    import org.jboss.seam.log.Log;
073    import org.jboss.seam.security.Identity;
074    import org.jboss.seam.util.Reflections;
075    
076    
077    /**
078     * @author William DRAI
079     */
080    public abstract class AbstractSeamServiceContext extends TideServiceContext {
081    
082        private static final long serialVersionUID = 1L;
083        
084        public static final String COMPONENT_NAME = "org.granite.tide.seam.serviceContext";
085        
086        protected @Logger Log log;
087        
088        private UserEvents userEvents;
089        private boolean isAsynchronousContext = true;
090        
091        
092        private static final String RESULTS_EVAL_ATTRIBUTE = AbstractSeamServiceContext.class.getName() + "_resultsEval";
093        private static final String RESULTS_EVAL_UNSPECIFIED_ATTRIBUTE = AbstractSeamServiceContext.class.getName() + "_resultsEval_unspecified";
094        
095        
096        public AbstractSeamServiceContext() throws ServiceException {
097            super();
098        }
099        
100        /**
101         * Determines the current sessionId for web invocations
102         */
103        @Override
104        public void initCall() {
105            super.initCall();
106            
107            if (userEvents != null)
108                return;
109            
110            if (getSessionId() != null)
111                userEvents = TideUserEvents.instance().getUserEvents(getSessionId());
112            else {
113                GraniteContext graniteContext = GraniteContext.getCurrentInstance();
114                if (graniteContext instanceof HttpGraniteContext) {
115                    HttpSession session = ((HttpGraniteContext)graniteContext).getSession(false);
116                    if (session != null)
117                        setSessionId(session.getId());
118                    isAsynchronousContext = false;
119                }
120            }
121        }
122        
123        /**
124         * Initialize current sessionId and event listeners for this context
125         * 
126         * @param sessionId current sessionId
127         */
128        @Override
129        public void setSessionId(String sessionId) {
130            super.setSessionId(sessionId);
131            userEvents = TideUserEvents.instance().getUserEvents(sessionId);
132        }
133        
134        /**
135         * Clear current session from user events registry
136         */
137        @Destroy
138        public void endSession() {
139            if (!isAsynchronousContext && getSessionId() != null)
140                TideUserEvents.instance().unregisterSession(getSessionId());
141        }
142        
143        
144        private Map<ContextResult, Boolean> getResultsEval(ScopeType scopeType) {
145            Context context = Contexts.getEventContext();
146            String att = RESULTS_EVAL_UNSPECIFIED_ATTRIBUTE;
147            if (scopeType == ScopeType.STATELESS)
148                    att = RESULTS_EVAL_ATTRIBUTE;
149            else if (scopeType != ScopeType.UNSPECIFIED) {
150                    context = scopeType.getContext();
151                    att = RESULTS_EVAL_ATTRIBUTE;
152            }
153            
154            @SuppressWarnings("unchecked")
155            Map<ContextResult, Boolean> resultsEval = (Map<ContextResult, Boolean>)context.get(att);
156            if (resultsEval == null) {
157                    resultsEval = new HashMap<ContextResult, Boolean>();
158                    context.set(att, resultsEval);
159            }
160            return resultsEval;
161        }
162        
163        
164        /**
165         * Constructs an asynchronous context object
166         * @return current context
167         */
168        public AsyncContext getAsyncContext() {
169            List<ContextResult> resultsEval = new ArrayList<ContextResult>();
170            for (ScopeType evalScopeType : EVAL_SCOPE_TYPES)
171                    resultsEval.addAll(getResultsEval(evalScopeType).keySet());
172            
173            return new AsyncContext(getSessionId(), resultsEval);
174        }
175        
176        /**
177         * Restores an asynchronous context
178         * @param asyncContext saved context
179         */
180        public void setAsyncContext(AsyncContext asyncContext) {
181            AsyncPublisher asyncPublisher = getAsyncPublisher();
182            if (asyncPublisher != null)
183                asyncPublisher.initThread();
184            
185            Contexts.getSessionContext().set("org.jboss.seam.security.identity", asyncContext.getIdentity());
186            setSessionId(asyncContext.getSessionId());
187            for (ContextResult resultEval : asyncContext.getResults()) {
188                    if (resultEval instanceof ScopedContextResult)
189                            getResultsEval(((ScopedContextResult)resultEval).getScope()).put(resultEval, Boolean.FALSE);
190                    else
191                            getResultsEval(ScopeType.UNSPECIFIED).put(resultEval, Boolean.FALSE);
192            }
193        }
194        
195        
196        /**
197         * Retrieve current messages
198         * 
199         * @return list of messages
200         */
201        protected abstract TideStatusMessages getTideMessages();
202        
203        protected abstract void initTideMessages();
204        
205        protected abstract void clearTideMessages();
206        
207        
208        /**
209         * Implementation of component lookup for Seam service
210         * 
211         * @param componentName component name
212         */
213        @Override
214        public Object findComponent(String componentName, Class<?> componentClass) {
215            Component component = TideInit.lookupComponent(componentName);
216            if (component == null)
217                return null;
218            return Component.getInstance(component.getName());
219        }
220        
221        /**
222         * Implementation of component lookup for Seam service
223         * 
224         * @param componentName component name
225         */
226        @Override
227        public Set<Class<?>> findComponentClasses(String componentName, Class<?> componentClass) {
228            Component component = TideInit.lookupComponent(componentName);
229            if (component == null)
230                return null;
231            
232            return componentClasses(component);
233        }
234        
235        private Set<Class<?>> componentClasses(Object component) {
236            if (component instanceof Component) {
237                    Set<Class<?>> classes = new HashSet<Class<?>>();
238                    for (Class<?> i : ((Component)component).getBusinessInterfaces())
239                            classes.add(i);
240                    classes.add(((Component)component).getBeanClass());
241                    return classes;
242            }
243            
244            Set<Class<?>> classes = new HashSet<Class<?>>(1);
245            classes.add(component.getClass());
246            return classes;
247        }
248        
249        
250        @Observer("org.jboss.seam.beginConversation")
251        public void observeBeginConversation() {
252            Contexts.getEventContext().set("org.granite.tide.conversation.wasCreated", true);
253        }
254        
255        
256        /**
257         * Add an event in the current context
258         * 
259         * @param type event type
260         * @param params event parameters
261         */
262        public void raiseEvent(String type, Object... params) {
263            // Add the event to the current invocation
264            TideInvocation tideInvocation = TideInvocation.get();
265            if (tideInvocation == null)
266                return;
267            
268            if (userEvents != null) {
269                // Avoid stupid infinite loop when creating locale selector as the log needs the current locale...
270                if (!type.endsWith("org.jboss.seam.international.localeSelector"))
271                    log.debug("Intercept event %s", type);
272                String sessionId = getSessionId();
273                if (sessionId != null && userEvents.hasEventType(type))
274                    tideInvocation.addEvent(new ContextEvent(type, params));
275            }
276            else if (Contexts.getSessionContext().isSet("org.granite.seam.login")) {
277                // Force send of all events raised during login
278                tideInvocation.addEvent(new ContextEvent(type, params));
279            }
280        }
281        
282        
283        /**
284         * Factory for Seam async publisher
285         * 
286         * @return servlet context of the current application
287         */
288        @Override
289        protected AsyncPublisher getAsyncPublisher() {
290            return (AsyncPublisher)Component.getInstance("org.granite.tide.seam.async.publisher");
291        }
292        
293        
294        /**
295         * Synchronizes server context with data provided by the client
296         * 
297         * @param context invocation context
298         * @param c client call
299         * @param componentName name of the component which will be invoked
300         */
301        @Override
302        public void prepareCall(ServiceInvocationContext context, IInvocationCall c, String componentName, Class<?> componentClass) {
303            InvocationCall call = (InvocationCall)c;
304            List<String> listeners = call.getListeners();
305            List<ContextUpdate> updates = call.getUpdates();
306            Object[] results = call.getResults();
307            
308            if (Contexts.isEventContextActive() && Contexts.isSessionContextActive() && 
309                            (Contexts.getSessionContext().isSet("org.granite.tide.isFirstCall") || Contexts.getSessionContext().isSet("org.granite.seam.login"))) {
310                // Login tried : force evaluation of existing session context
311                for (Map.Entry<ContextResult, Boolean> me : getResultsEval(ScopeType.SESSION).entrySet()) {
312                    if (me.getKey().getExpression() == null && Contexts.getSessionContext().isSet(me.getKey().getComponentName()))
313                        me.setValue(Boolean.TRUE);
314                }
315                Contexts.getSessionContext().remove("org.granite.seam.login");
316                Contexts.getSessionContext().remove("org.granite.tide.isFirstCall");
317                
318                // Force quiet login
319                if (Identity.instance().isLoggedIn() && Contexts.isEventContextActive())
320                   Contexts.getEventContext().set("org.jboss.seam.security.silentLogin", true);
321            }
322            if (Contexts.isEventContextActive() && Contexts.isConversationContextActive() && 
323                            Contexts.getConversationContext().isSet("org.granite.tide.isFirstConversationCall")) {
324                // Join conversation : force evaluation of existing conversation context
325                for (Map.Entry<ContextResult, Boolean> me : getResultsEval(ScopeType.CONVERSATION).entrySet()) {
326                    if (me.getKey().getExpression() == null && Contexts.getConversationContext().isSet(me.getKey().getComponentName()))
327                        me.setValue(Boolean.TRUE);
328                }
329                Contexts.getConversationContext().remove("org.granite.tide.isFirstConversationCall");
330            }
331            
332            String sessionId = getSessionId();
333            if (sessionId != null && listeners != null) {
334                // Registers new event listeners
335                for (String listener : listeners)
336                    TideUserEvents.instance().registerEventType(sessionId, listener);
337                
338                if (userEvents == null)
339                    userEvents = TideUserEvents.instance().getUserEvents(getSessionId());
340            }
341            
342            if (results != null) {
343                    Map<ContextResult, Boolean> resultsEval = getResultsEval(ScopeType.UNSPECIFIED);
344                for (Object result : results) {
345                    ContextResult cr = (ContextResult)result;
346                    resultsEval.put(new ScopedContextResult(cr.getComponentName(), cr.getExpression(), ScopeType.UNSPECIFIED, null), Boolean.TRUE);
347                    
348        //            if (!factories.containsKey(cr.getComponentName())) {
349        //                FactoryExpression expr = Init.instance().getFactoryValueExpression(cr.getComponentName());
350        //                if (expr != null) {
351        //                    String vexpr = expr.getValueBinding().getExpressionString();
352        //                    vexpr = vexpr.substring(2, vexpr.indexOf('.', 2));
353        //                    factories.put(cr.getComponentName(), vexpr);
354        //                    
355        //                    resultsEval.put(new ContextResult(cr.getComponentName(), null), Boolean.TRUE);
356        //                }
357        //            }
358                }
359            }
360            
361            boolean instrumented = false;
362            Component component = null;
363            if (componentName != null) {
364                component = TideInit.lookupComponent(componentName);
365                if (component.isInterceptionEnabled())
366                    instrumented = true;
367                
368                // Forces evaluation of all results if they are related to the called component
369                for (Map.Entry<ContextResult, Boolean> me : getResultsEval(component.getScope()).entrySet()) {
370                    if (me.getKey().getComponentName().equals(componentName))
371    //                    || componentName.equals(factories.get(me.getKey().getComponentName())))
372                        me.setValue(Boolean.TRUE);
373                }
374            }
375            
376            initTideMessages();
377            
378            SeamInitializer.instance().restoreLoadedEntities();
379            
380            if (Conversation.instance().isLongRunning()) {
381                // Merge call arguments with current conversation context
382                Object[] args = context.getParameters();
383                if (args != null) {
384                        for (int i = 0; i < args.length; i++) {
385                            Object value = mergeExternal(args[i], null);
386                            args[i] = value;
387                        }
388                }
389            }
390            
391            TideInvocation tideInvocation = TideInvocation.init();
392            tideInvocation.update(updates);
393            
394            if (!instrumented) {
395                // If no interception enabled, force the update of the context for the current component
396                // In other cases it will be done by the interceptor 
397                restoreContext(updates, component, null);
398                tideInvocation.updated();
399            }
400        }
401    
402        /**
403         * Builds the result object for the invocation
404         * 
405         * @param context invocation context
406         * @param result result of the method invocation
407         * @param componentName name of the invoked component
408         * @return result object
409         */
410        @Override
411        public IInvocationResult postCall(ServiceInvocationContext context, Object result, String componentName, Class<?> componentClass) {
412            TideInvocation tideInvocation = TideInvocation.get();
413            List<ContextUpdate> results = null;
414            int scope = 3;
415            boolean restrict = false;
416            
417            Component component = null;
418            if (componentName != null) {
419                // Determines scope of component
420                component = TideInit.lookupComponent(componentName);
421                if (Contexts.isMethodContextActive() && Contexts.getMethodContext().isSet(component.getName()))
422                    scope = 3;
423                else if (Contexts.isEventContextActive() && Contexts.getEventContext().isSet(component.getName()))
424                    scope = 3;
425                else if (Contexts.isPageContextActive() && Contexts.getPageContext().isSet(component.getName()))
426                    scope = 3;
427                else if (Contexts.isConversationContextActive() && Contexts.getConversationContext().isSet(component.getName()))
428                    scope = 2;
429                
430                restrict = component.beanClassHasAnnotation(Restrict.class);
431            }
432            
433            if (!tideInvocation.isEvaluated()) {
434                // Do evaluation now if the interceptor has not been called
435                results = evaluateResults(null, null, componentName == null 
436                            && !(context != null && context.getMethod() != null && "resyncContext".equals(context.getMethod().getName())));
437            }
438            else
439                results = tideInvocation.getResults();
440            
441            DataContext dataContext = DataContext.get();
442                    Set<Object[]> dataUpdates = dataContext != null ? dataContext.getDataUpdates() : null;
443                    Object[][] updates = null;
444                    if (dataUpdates != null && !dataUpdates.isEmpty())
445                            updates = dataUpdates.toArray(new Object[dataUpdates.size()][]);
446                    
447            InvocationResult res = new InvocationResult(result, results);
448            res.setScope(scope);
449            res.setRestrict(restrict);
450            if (component != null) {
451                    if (component.beanClassHasAnnotation(BypassTideMerge.class))
452                            res.setMerge(false);
453                    else {
454                            try {
455                                    if (context != null) {
456                                            Method m = component.getBeanClass().getMethod(context.getMethod().getName(), context.getMethod().getParameterTypes());
457                                            if (m.isAnnotationPresent(BypassTideMerge.class))
458                                                    res.setMerge(false);
459                                    }
460                            }
461                            catch (Exception e) {
462                                    log.warn("Could not find bean method", e);
463                            }
464                    }
465            }
466            
467            if (Conversation.instance().isLongRunning()) {
468                    // Put results in merge context to keep data in extended persistence context between remote calls
469                    DataMergeContext.addResultEntity(result);
470                    for (ContextUpdate cu : results)
471                            DataMergeContext.addResultEntity(cu.getValue());
472            }
473            
474            res.setUpdates(updates);
475            
476            // Adds events in result object
477            res.setEvents(tideInvocation.getEvents());
478            
479            // Save current set of entities loaded in a conversation scoped component to handle case of extended PM
480            SeamInitializer.instance().saveLoadedEntities();
481            
482            // Adds context messages in result object
483            TideStatusMessages statusMessages = getTideMessages();
484            res.setMessages(statusMessages.getMessages());
485            res.setKeyedMessages(statusMessages.getKeyedMessages());
486            
487            clearTideMessages();
488    
489            // Clean thread
490            TideInvocation.remove();
491            
492            return res;
493        }
494    
495        /**
496         * Intercepts a fault on the invocation
497         * 
498         * @param context invocation context
499         * @param t exception thrown
500         * @param componentName name of the invoked component
501         */
502        @Override
503        public void postCallFault(ServiceInvocationContext context, Throwable t, String componentName, Class<?> componentClass) {
504            clearTideMessages();
505            
506            // Clean thread: very important to avoid phantom evaluations after exceptions
507            TideInvocation.remove();
508        }
509        
510        
511        public void addResultEval(ScopedContextResult result) {
512            getResultsEval(result.getScope()).put(result, Boolean.TRUE);
513        }
514        
515        
516        /**
517         * Evaluate updates in current server context
518         * 
519         * @param updates list of updates
520         * @param component the target component
521         * @param target the target instance
522         */
523        public void restoreContext(List<ContextUpdate> updates, Component component, Object target) {
524            if (updates == null)
525                return;
526            
527            GraniteConfig config = GraniteContext.getCurrentInstance().getGraniteConfig();
528            
529            // Restore context
530            for (ContextUpdate update : updates) {
531                
532                log.debug("Before invocation: evaluating expression #0.#1", update.getComponentName(), update.getExpression());
533                
534                Component sourceComponent = TideInit.lookupComponent(update.getComponentName());
535                String sourceComponentName = sourceComponent != null ? sourceComponent.getName() : update.getComponentName();
536                
537                Object previous = null;
538                if (update.getExpression() != null) {
539                    // Set component property
540                    // Ignore expression on external components if target component is not interception enabled 
541                    // to avoid issues with bijection, persistence contexts and transactions
542                    if (target != null || (component != null && component.getName().equals(sourceComponentName))) {
543                        // Get current values in Seam context
544                        String[] path = update.getExpression().split("\\.");
545                        Object instance = component != null && component.getName().equals(sourceComponentName) && target != null 
546                            ? target 
547                            : Component.getInstance(sourceComponentName);
548                        boolean disabled = instance != null && sourceComponent != null && 
549                            config.isComponentTideDisabled(sourceComponentName, componentClasses(sourceComponent), instance);
550                        if (!disabled) {
551                            Object bean = instance;
552                            Object value = instance;
553                            
554                            List<Field> dmsFields = instance != null && path.length == 1 
555                                    ? org.granite.util.Reflections.getFields(instance.getClass(), DataModelSelection.class) : null;
556                            List<String> dmsFieldNames = null;
557                            if (dmsFields != null && !dmsFields.isEmpty()) {
558                                    dmsFieldNames = new ArrayList<String>(dmsFields.size());
559                                for (Field f : dmsFields)
560                                    dmsFieldNames.add(f.getName());
561                            }
562                            
563                            if (update.getValue() != null) {
564                                boolean getPrevious = true;
565                                if (update.getValue().getClass().getAnnotation(Entity.class) != null) {
566                                    org.granite.util.Entity entity = new org.granite.util.Entity(update.getValue());
567                                    if (entity.getIdentifier() == null)
568                                        getPrevious = false;
569                                }
570                                if (getPrevious) {
571                                    try {
572                                        for (int i = 0; i < path.length; i++) {
573                                            if (value == null)
574                                                break;
575                                            if (i == 0 && dmsFieldNames != null && dmsFieldNames.contains(path[0])) {
576                                                    Field field = org.granite.util.Reflections.getField(value.getClass(), path[i]);
577                                                    field.setAccessible(true);
578                                                    value = Reflections.get(field, value);
579                                                if (i < path.length-1)
580                                                    bean = value;
581                                            }
582                                            else {
583                                                // Use modified Reflections for getter because of a bug in Seam 2.0.0
584                                                Method getter = org.granite.util.Reflections.getGetterMethod(value.getClass(), path[i]);
585                                                value = Reflections.invoke(getter, value);
586                                                if (i < path.length-1)
587                                                    bean = value;
588                                            }
589                                        }
590                                    }
591                                    catch (IllegalArgumentException e) {
592                                        // No getter found to retrieve current value
593                                        log.warn("Partial merge only: " + e.getMessage());
594                                        value = null;
595                                    }
596                                        catch (Exception e) {
597                                            throw new RuntimeException("Could not get property: " + update.toString(), e);
598                                        }
599                                    previous = value;
600                                }
601                            }
602                            
603                            // Set new value
604                            try {
605                                    if (bean != null && path.length == 1 && dmsFieldNames != null && dmsFieldNames.contains(path[0])) {
606                                            Field field = org.granite.util.Reflections.getField(bean.getClass(), path[0]);
607                                            field.setAccessible(true);
608                                        value = GraniteContext.getCurrentInstance().getGraniteConfig().getConverters().convert(update.getValue(), field.getType());
609                                        // Merge entities into current persistent context if needed
610                                        value = mergeExternal(value, previous);
611                                            Reflections.set(field, bean, value);
612                                    }
613                                    else if (bean != null) {
614                                        Method setter = Reflections.getSetterMethod(bean.getClass(), path[path.length-1]);
615                                        Type type = setter.getParameterTypes()[0];
616                                            if (bean instanceof Home<?, ?> && "id".equals(path[path.length-1])) {
617                                                    // Special (ugly ?) handling for Home object to try to guess id type (setId is of type Object)
618                                                    try {
619                                                            Class<?> entityClass = ((Home<?, ?>)bean).getEntityClass();
620                                                            org.granite.util.Entity entity = new org.granite.util.Entity(entityClass);
621                                                            type = entity.getIdentifierType();
622                                                    }
623                                                    catch (Exception e) {
624                                                            // Ignore
625                                                    }
626                                            }
627                                        value = GraniteContext.getCurrentInstance().getGraniteConfig().getConverters().convert(update.getValue(), type);
628                                        // Merge entities into current persistent context if needed
629                                        value = mergeExternal(value, previous);
630                                        Reflections.invoke(setter, bean, value);
631                                    }
632                                }
633                                catch (Exception e) {
634                                    throw new RuntimeException("Could not restore property: " + update.toString(), e);
635                                }
636                        }
637                    }
638                }
639                else {
640                    // Set context variable
641                    if (sourceComponent != null) {
642                        ScopeType scope = sourceComponent.getScope();
643                        if (!((update.getScope() == 2 && (scope == ScopeType.EVENT 
644                                    || scope == ScopeType.STATELESS 
645                                    || scope == ScopeType.CONVERSATION
646                                    || scope == ScopeType.BUSINESS_PROCESS
647                                    || scope == ScopeType.METHOD
648                                    || scope == ScopeType.PAGE))
649                            || (update.getScope() == 1 && (scope == ScopeType.EVENT
650                                    || scope == ScopeType.STATELESS 
651                                    || scope == ScopeType.SESSION
652                                    || scope == ScopeType.METHOD
653                                    || scope == ScopeType.PAGE))
654                            || (update.getScope() == 3 && (scope == ScopeType.EVENT
655                                            || scope == ScopeType.STATELESS
656                                            || scope == ScopeType.METHOD
657                                            || scope == ScopeType.PAGE)))) {
658                            scope = ScopeType.EVENT;
659                        }
660                        
661                        previous = scope.getContext().get(sourceComponentName);
662                        
663                        boolean disabled = previous != null && config.isComponentTideDisabled(sourceComponentName, componentClasses(sourceComponent), previous);
664                        if (!disabled) {
665                            Object value = mergeExternal(update.getValue(), previous);
666                            
667                            scope.getContext().set(sourceComponentName, value);
668                        }
669                    }
670                    else {
671                        Object[] prev = lookupInStatefulContexts(sourceComponentName, ScopeType.UNSPECIFIED);
672                        ScopeType scope = ScopeType.UNSPECIFIED;
673                        if (prev != null) {
674                            previous = prev[0];
675                            scope = (ScopeType)prev[1];
676                        }
677                        
678                        boolean disabled = previous != null && config.isComponentTideDisabled(sourceComponentName, 
679                                    componentClasses(previous), previous);
680                        if (!disabled) {
681                            if (scope == ScopeType.UNSPECIFIED) {
682                                scope = ScopeType.EVENT;
683                                scope.getContext().set(sourceComponentName + "_tide_unspecified_", true);
684                            }
685                            
686                            Object value = mergeExternal(update.getValue(), previous);
687                            
688                            scope.getContext().set(sourceComponentName, value);
689                        }
690                    }
691                }
692            }
693        }
694        
695        
696        private static final ScopeType[] EVAL_SCOPE_TYPES = {
697            ScopeType.UNSPECIFIED,
698            ScopeType.EVENT,
699            ScopeType.CONVERSATION,
700            ScopeType.SESSION,
701            ScopeType.BUSINESS_PROCESS,
702            ScopeType.APPLICATION
703        };
704        
705        /**
706         * Evaluate results from context
707         * 
708         * @param component the target component
709         * @param target the target instance
710         * @param nothing used by initializer to avoid interactions with context sync
711         * 
712         * @return list of updates to send back to the client
713         */
714        public List<ContextUpdate> evaluateResults(Component component, Object target, boolean nothing) {
715            
716            List<ContextUpdate> resultsMap = new ArrayList<ContextUpdate>();
717            
718            if (nothing)
719                return resultsMap;
720            
721            List<String> exprs = new ArrayList<String>();
722            GraniteConfig config = GraniteContext.getCurrentInstance().getGraniteConfig();
723            ClassGetter classGetter = GraniteContext.getCurrentInstance().getGraniteConfig().getClassGetter();
724            
725            SeamInitializer.instance();
726            
727            for (ScopeType evalScopeType : EVAL_SCOPE_TYPES) {
728                    for (Map.Entry<ContextResult, Boolean> me : getResultsEval(evalScopeType).entrySet()) {
729                        if (!me.getValue())
730                            continue;
731                        
732                        ContextResult res = me.getKey();
733                        Component targetComponent = TideInit.lookupComponent(res.getComponentName());
734                        
735                        String targetComponentName = targetComponent != null ? targetComponent.getName() : res.getComponentName();
736                        
737                        if (res.getExpression() != null && component != null && targetComponent != null && !(component.getName().equals(targetComponentName))) {
738                            // Ignore results concerning other components for this time
739                            // In this case is has to be a non interception enabled component
740                            continue;
741                        }
742                        
743                        int idx = 0;
744                        boolean add = true;
745                        
746                        // Force evaluation of consecutive properties
747                        while (idx >= 0) {
748                            idx = res.getExpression() != null ? res.getExpression().indexOf(".", idx+1) : -1;
749                            
750                            String expr = (idx > 0 ? res.getExpression().substring(0, idx) : res.getExpression());
751                            String ex = expr != null ? res.getComponentName() + "." + expr : res.getComponentName();
752                            
753                            if (!exprs.contains(ex)) {
754                                log.debug("After invocation: evaluating expression #0", ex);
755                                
756                                String[] path = expr != null ? expr.split("\\.") : new String[0];
757                                
758                                try {
759                                    Object value = null;
760                                    ScopeType scopeType = res instanceof ScopedContextResult ? ((ScopedContextResult)res).getScope() : ScopeType.UNSPECIFIED;
761                                    Boolean restrict = res.getRestrict();
762                                    
763                                FactoryMethod factoryMethod = null;
764                                FactoryExpression factoryExpression = null;
765                                if (targetComponent == null) {
766                                    factoryExpression = TideInit.lookupFactoryExpression(res.getComponentName());
767                                    if (factoryExpression == null)
768                                            factoryMethod = TideInit.lookupFactory(res.getComponentName());
769                                }
770                                
771                                if (targetComponent != null) {
772                                    if (component != null && component.getName().equals(targetComponent.getName())) {
773                                            value = target;
774                                            scopeType = targetComponent.getScope();
775                                        }
776                                    else if (ScopeType.UNSPECIFIED.equals(scopeType)) {
777                                        value = Component.getInstance(targetComponent.getName());
778                                        scopeType = targetComponent.getScope();
779                                        if (ScopeType.STATELESS.equals(scopeType))
780                                            scopeType = ScopeType.EVENT;
781                                        
782                                        if (value != null && config.isComponentTideDisabled(targetComponentName, 
783                                                    componentClasses(targetComponent), value))
784                                            add = false;
785                                    }
786                                    else {
787                                        value = Component.getInstance(targetComponent.getName(), scopeType);
788                                        
789                                        if (value != null && config.isComponentTideDisabled(targetComponentName, 
790                                                    componentClasses(value), value))
791                                            add = false;
792                                    }
793                                    
794                                    restrict = targetComponent.beanClassHasAnnotation(Restrict.class);
795                                }
796                                else if (factoryExpression != null) {
797                                    String expressionString = factoryExpression.getMethodBinding() != null 
798                                            ? factoryExpression.getMethodBinding().getExpressionString() 
799                                            : factoryExpression.getValueBinding().getExpressionString();
800                                    int iedx = expressionString.indexOf(".");
801                                    String expressionBaseName = expressionString.substring(2, iedx);
802                                    
803                                    if (ScopeType.UNSPECIFIED.equals(scopeType)) {
804                                        value = Component.getInstance(res.getComponentName());
805                                        scopeType = factoryExpression.getScope();
806                                        if (ScopeType.STATELESS.equals(scopeType))
807                                            scopeType = ScopeType.EVENT;
808                                        
809                                        if (value != null && config.isComponentTideDisabled(expressionBaseName, componentClasses(value), value))
810                                            add = false;
811                                    }
812                                    else {
813                                        value = Component.getInstance(res.getComponentName(), scopeType);
814                                        
815                                        if (value != null && config.isComponentTideDisabled(expressionBaseName, componentClasses(value), value))
816                                            add = false;
817                                    }
818                                    
819                                    Component factoryComponent = TideInit.lookupComponent(expressionBaseName);
820                                    restrict = factoryComponent != null ? factoryComponent.beanClassHasAnnotation(Restrict.class) : false;
821                                }
822                                else if (factoryMethod != null) {
823                                    if (ScopeType.UNSPECIFIED.equals(scopeType)) {
824                                        value = Component.getInstance(factoryMethod.getMethod().getAnnotation(Factory.class).value());
825                                        scopeType = factoryMethod.getScope();
826                                        if (ScopeType.STATELESS.equals(scopeType))
827                                            scopeType = ScopeType.EVENT;
828                                        
829                                        if (value != null && config.isComponentTideDisabled(factoryMethod.getComponent().getName(), 
830                                                    componentClasses(factoryMethod.getComponent()), value))
831                                            add = false;
832                                    }
833                                    else {
834                                        value = Component.getInstance(res.getComponentName(), scopeType);
835                                        
836                                        if (value != null && config.isComponentTideDisabled(factoryMethod.getComponent().getName(), 
837                                                    componentClasses(value), value))
838                                            add = false;
839                                    }
840    
841                                    restrict = factoryMethod.getComponent().beanClassHasAnnotation(Restrict.class);
842                                }
843                                else {
844                                    Object[] val = lookupInStatefulContexts(res.getComponentName(), scopeType);
845                                    if (val != null) {
846                                        value = val[0];
847                                        scopeType = (ScopeType)val[1];
848                                        
849                                        if (value != null && config.isComponentTideDisabled(res.getComponentName(), 
850                                                    componentClasses(value), value))
851                                            add = false;
852                                    }
853                                }
854                                    
855                                if (add) {
856                                    Object v0 = null;
857                                    String propName = null;
858                                    for (int i = 0; i < path.length; i++) {
859                                        if (value == null)
860                                            break;
861                                        // Use modified Reflections for getter because of a bug in Seam 2.0.0
862                                        v0 = value;
863                                        propName = path[i];
864                                        Method getter = org.granite.util.Reflections.getGetterMethod(value.getClass(), path[i]);
865                                        value = Reflections.invoke(getter, value);
866                                    }
867                                    
868                                            getResultsEval(scopeType).put(res, false);
869                                            
870                                        if (value instanceof TideDataModel) {
871                                            // Unwrap value
872                                            value = ((TideDataModel)value).getWrappedData();
873                                        }
874                                        else if (value != null) {
875                                            if (classGetter != null) {
876                                                classGetter.initialize(v0, propName, value);
877                                                if (res.getExpression() != null) {
878                                                    String[] fullPath = res.getExpression().split("\\.");
879                                                    Object v = value;
880                                                    for (int i = path.length; i < fullPath.length; i++) {
881                                                        // Use modified Reflections for getter because of a bug in Seam 2.0.0
882                                                        Method getter = org.granite.util.Reflections.getGetterMethod(v.getClass(), fullPath[i]);
883                                                        v0 = v;
884                                                        v = Reflections.invoke(getter, v);
885    //                                                  if (v == null)
886    //                                                      break;
887                                                        classGetter.initialize(v0, fullPath[i], v);
888                                                        if (v == null)
889                                                            break;
890                                                    }
891                                                }
892                                            }
893                                        }
894                                    
895                                        int scope = (scopeType == ScopeType.CONVERSATION ? 2 : (scopeType == ScopeType.SESSION ? 1 : 3));
896                                        
897                                        resultsMap.add(new ContextUpdate(res.getComponentName(), expr, value, scope, Boolean.TRUE.equals(restrict)));
898                                        add = false;
899                                    }
900                                    
901                                    exprs.add(ex);
902                                }
903                            catch (IllegalArgumentException e) {
904                                    log.debug("Evaluate result error for " + res.toString(), e);
905                                    // GDS-566
906                            }
907                                catch (Exception e) {
908                                    throw new RuntimeException("Could not evaluate result expression: " + ex, e);
909                                }
910                            }
911                        }
912                        
913                        me.setValue(Boolean.FALSE);
914                    }
915            }
916            
917            return resultsMap;
918        }
919        
920        
921        /**
922         * Implementations of intercepted asynchronous calls
923         * Send asynchronous event to client
924         * @param asyncContext current context (session id)
925         * @param targetComponentName target component name
926         * @param methodName method name
927         * @param paramTypes method argument types
928         * @param params argument values
929         * @return result
930         */
931        public Object invokeAsynchronous(AsyncContext asyncContext, String targetComponentName, Class<?> targetComponentClass, String methodName, Class<?>[] paramTypes, Object[] params) {
932            setAsyncContext(asyncContext);
933            
934            // Just another ugly hack: the Seam interceptor has set this variable and we don't want it
935            Contexts.getEventContext().remove("org.jboss.seam.async.AsynchronousIntercepter.REENTRANT");
936            
937            Component component = TideInit.lookupComponent(targetComponentName);
938            
939            // Forces evaluation of all results if they are related to the called component
940            for (Map.Entry<ContextResult, Boolean> me : getResultsEval(component.getScope()).entrySet()) {
941                if (me.getKey().getComponentName().equals(targetComponentName))
942                    me.setValue(Boolean.TRUE);
943            }
944            
945            Object target = Component.getInstance(targetComponentName);
946            
947            Method method;
948            try {
949                method = target.getClass().getMethod(methodName, paramTypes);
950            }
951            catch (NoSuchMethodException nsme) {
952               throw new IllegalStateException(nsme);
953            }
954            
955            Object result = Reflections.invokeAndWrap(method, target, params);
956            
957            sendEvent(targetComponentName, targetComponentClass);
958            
959            return result;
960        }
961        
962    
963        @Override
964        protected TidePersistenceManager getTidePersistenceManager(boolean create) {
965            return SeamInitializer.instance().getTidePersistenceManager(); 
966        }
967        
968        
969        /**
970         * Search for a named attribute in all contexts, in the
971         * following order: method, event, page, conversation,
972         * session, business process, application.
973         * 
974         * @return the first component found, or null
975         */
976        public static Object[] lookupInStatefulContexts(String name, ScopeType scope) {
977            if ((ScopeType.UNSPECIFIED.equals(scope) || ScopeType.METHOD.equals(scope)) && Contexts.isMethodContextActive()) {
978                Object result = Contexts.getMethodContext().get(name);
979                if (result != null)
980                    return new Object[] { result, Contexts.getMethodContext().getType() };
981            }
982            
983            if ((ScopeType.UNSPECIFIED.equals(scope) || ScopeType.EVENT.equals(scope)) && Contexts.isEventContextActive()) {
984                Object result = Contexts.getEventContext().get(name);
985                if (result != null)
986                    return new Object[] { result, Contexts.getEventContext().getType() };
987            }
988            
989            if ((ScopeType.UNSPECIFIED.equals(scope) || ScopeType.PAGE.equals(scope)) && Contexts.isPageContextActive()) {
990                Object result = Contexts.getPageContext().get(name);
991                if (result != null)
992                    return new Object[] { result, Contexts.getPageContext().getType() };
993            }
994            
995            if ((ScopeType.UNSPECIFIED.equals(scope) || ScopeType.CONVERSATION.equals(scope)) && Contexts.isConversationContextActive()) {
996                Object result = Contexts.getConversationContext().get(name);
997                if (result != null)
998                    return new Object[] { result, Contexts.getConversationContext().getType() };
999            }
1000            
1001            if ((ScopeType.UNSPECIFIED.equals(scope) || ScopeType.SESSION.equals(scope)) && Contexts.isSessionContextActive()) {
1002                Object result = Contexts.getSessionContext().get(name);
1003                if (result != null)
1004                    return new Object[] { result, Contexts.getSessionContext().getType() };
1005            }
1006            
1007            if ((ScopeType.UNSPECIFIED.equals(scope) || ScopeType.BUSINESS_PROCESS.equals(scope)) && Contexts.isBusinessProcessContextActive()) {
1008                Object result = Contexts.getBusinessProcessContext().get(name);
1009                if (result != null)
1010                    return new Object[] { result, Contexts.getBusinessProcessContext().getType() };
1011            }
1012            
1013            if ((ScopeType.UNSPECIFIED.equals(scope) || ScopeType.APPLICATION.equals(scope)) && Contexts.isApplicationContextActive()) {
1014                Object result = Contexts.getApplicationContext().get(name);
1015                if (result != null)
1016                    return new Object[] { result, Contexts.getApplicationContext().getType() };
1017            }
1018            
1019            return ScopeType.UNSPECIFIED.equals(scope) ? null : new Object[] { null, scope };
1020        }
1021    }