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