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
021package org.granite.tide.seam;
022
023import java.lang.reflect.Field;
024import java.lang.reflect.Method;
025import java.lang.reflect.Type;
026import java.util.ArrayList;
027import java.util.HashMap;
028import java.util.HashSet;
029import java.util.List;
030import java.util.Map;
031import java.util.Set;
032
033import javax.persistence.Entity;
034import javax.servlet.http.HttpSession;
035
036import org.granite.config.GraniteConfig;
037import org.granite.context.GraniteContext;
038import org.granite.messaging.amf.io.util.ClassGetter;
039import org.granite.messaging.service.ServiceException;
040import org.granite.messaging.service.ServiceInvocationContext;
041import org.granite.messaging.webapp.HttpGraniteContext;
042import org.granite.tide.IInvocationCall;
043import org.granite.tide.IInvocationResult;
044import org.granite.tide.TidePersistenceManager;
045import org.granite.tide.TideServiceContext;
046import org.granite.tide.TideStatusMessages;
047import org.granite.tide.annotations.BypassTideMerge;
048import org.granite.tide.async.AsyncPublisher;
049import org.granite.tide.data.DataContext;
050import org.granite.tide.data.DataMergeContext;
051import org.granite.tide.data.DataUpdatePostprocessor;
052import org.granite.tide.invocation.ContextEvent;
053import org.granite.tide.invocation.ContextResult;
054import org.granite.tide.invocation.ContextUpdate;
055import org.granite.tide.invocation.InvocationCall;
056import org.granite.tide.invocation.InvocationResult;
057import org.granite.tide.seam.async.AsyncContext;
058import org.granite.tide.seam.lazy.SeamInitializer;
059import org.jboss.seam.Component;
060import org.jboss.seam.ScopeType;
061import org.jboss.seam.annotations.Destroy;
062import org.jboss.seam.annotations.Factory;
063import org.jboss.seam.annotations.Logger;
064import org.jboss.seam.annotations.Observer;
065import org.jboss.seam.annotations.datamodel.DataModelSelection;
066import org.jboss.seam.annotations.security.Restrict;
067import org.jboss.seam.contexts.Context;
068import org.jboss.seam.contexts.Contexts;
069import org.jboss.seam.core.Conversation;
070import org.jboss.seam.core.Init.FactoryExpression;
071import org.jboss.seam.core.Init.FactoryMethod;
072import org.jboss.seam.framework.Home;
073import org.jboss.seam.log.Log;
074import org.jboss.seam.security.Identity;
075import org.jboss.seam.util.Reflections;
076
077
078/**
079 * @author William DRAI
080 */
081public 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 ires = new InvocationResult(result, results);
456        ires.setScope(scope);
457        ires.setRestrict(restrict);
458        if (component != null) {
459                if (component.beanClassHasAnnotation(BypassTideMerge.class) || component.businessInterfaceHasAnnotation(BypassTideMerge.class))
460                        ires.setMerge(false);
461                else if (context != null) {
462                        try {
463                                Method m = component.getBeanClass().getMethod(context.getMethod().getName(), context.getMethod().getParameterTypes());
464                                if (m.isAnnotationPresent(BypassTideMerge.class))
465                                        ires.setMerge(false);
466                        }
467                        catch (NoSuchMethodException e) {
468                                log.warn("Could not find bean method", e);
469                        }
470                        
471                        for (Class<?> beanInterface : component.getBusinessInterfaces()) {
472                        try {
473                                Method m = beanInterface.getMethod(context.getMethod().getName(), context.getMethod().getParameterTypes());
474                                if (m.isAnnotationPresent(BypassTideMerge.class)) {
475                                        ires.setMerge(false);
476                                        break;
477                                }
478                        }
479                        catch (NoSuchMethodException e) {
480                                log.warn("Could not find bean method", e);
481                        }
482                        }
483                }
484        }
485        
486        if (Conversation.instance().isLongRunning()) {
487                // Put results in merge context to keep data in extended persistence context between remote calls
488                DataMergeContext.addResultEntity(result);
489                for (ContextUpdate cu : results)
490                        DataMergeContext.addResultEntity(cu.getValue());
491        }
492        
493        ires.setUpdates(updates);
494        
495        // Adds events in result object
496        ires.setEvents(tideInvocation.getEvents());
497        
498        // Save current set of entities loaded in a conversation scoped component to handle case of extended PM
499        SeamInitializer.instance().saveLoadedEntities();
500        
501        // Adds context messages in result object
502        TideStatusMessages statusMessages = getTideMessages();
503        ires.setMessages(statusMessages.getMessages());
504        ires.setKeyedMessages(statusMessages.getKeyedMessages());
505        
506        clearTideMessages();
507
508        // Clean thread
509        TideInvocation.remove();
510        
511        return ires;
512    }
513
514    /**
515     * Intercepts a fault on the invocation
516     * 
517     * @param context invocation context
518     * @param t exception thrown
519     * @param componentName name of the invoked component
520     */
521    @Override
522    public void postCallFault(ServiceInvocationContext context, Throwable t, String componentName, Class<?> componentClass) {
523        clearTideMessages();
524        
525        // Clean thread: very important to avoid phantom evaluations after exceptions
526        TideInvocation.remove();
527    }
528    
529    
530    public void addResultEval(ScopedContextResult result) {
531        getResultsEval(result.getScope()).put(result, Boolean.TRUE);
532    }
533    
534    
535    /**
536     * Evaluate updates in current server context
537     * 
538     * @param updates list of updates
539     * @param component the target component
540     * @param target the target instance
541     */
542    public void restoreContext(List<ContextUpdate> updates, Component component, Object target) {
543        if (updates == null)
544            return;
545        
546        GraniteConfig config = GraniteContext.getCurrentInstance().getGraniteConfig();
547        
548        // Restore context
549        for (ContextUpdate update : updates) {
550            
551            log.debug("Before invocation: evaluating expression #0.#1", update.getComponentName(), update.getExpression());
552            
553            Component sourceComponent = TideInit.lookupComponent(update.getComponentName());
554            String sourceComponentName = sourceComponent != null ? sourceComponent.getName() : update.getComponentName();
555            
556            Object previous = null;
557            if (update.getExpression() != null) {
558                // Set component property
559                // Ignore expression on external components if target component is not interception enabled 
560                // to avoid issues with bijection, persistence contexts and transactions
561                if (target != null || (component != null && component.getName().equals(sourceComponentName))) {
562                    // Get current values in Seam context
563                    String[] path = update.getExpression().split("\\.");
564                    Object instance = component != null && component.getName().equals(sourceComponentName) && target != null 
565                        ? target 
566                        : Component.getInstance(sourceComponentName);
567                    boolean disabled = instance != null && sourceComponent != null && 
568                        config.isComponentTideDisabled(sourceComponentName, componentClasses(sourceComponent), instance);
569                    if (!disabled) {
570                        Object bean = instance;
571                        Object value = instance;
572                        
573                        List<Field> dmsFields = instance != null && path.length == 1 
574                                ? org.granite.util.Reflections.getFields(instance.getClass(), DataModelSelection.class) : null;
575                        List<String> dmsFieldNames = null;
576                        if (dmsFields != null && !dmsFields.isEmpty()) {
577                                dmsFieldNames = new ArrayList<String>(dmsFields.size());
578                            for (Field f : dmsFields)
579                                dmsFieldNames.add(f.getName());
580                        }
581                        
582                        if (update.getValue() != null) {
583                            boolean getPrevious = true;
584                            if (update.getValue().getClass().getAnnotation(Entity.class) != null) {
585                                org.granite.util.Entity entity = new org.granite.util.Entity(update.getValue());
586                                if (entity.getIdentifier() == null)
587                                    getPrevious = false;
588                            }
589                            if (getPrevious) {
590                                try {
591                                    for (int i = 0; i < path.length; i++) {
592                                        if (value == null)
593                                            break;
594                                        if (i == 0 && dmsFieldNames != null && dmsFieldNames.contains(path[0])) {
595                                                Field field = org.granite.util.Reflections.getField(value.getClass(), path[i]);
596                                                field.setAccessible(true);
597                                                value = Reflections.get(field, value);
598                                            if (i < path.length-1)
599                                                bean = value;
600                                        }
601                                        else {
602                                            // Use modified Reflections for getter because of a bug in Seam 2.0.0
603                                            Method getter = org.granite.util.Reflections.getGetterMethod(value.getClass(), path[i]);
604                                            value = Reflections.invoke(getter, value);
605                                            if (i < path.length-1)
606                                                bean = value;
607                                        }
608                                    }
609                                }
610                                catch (IllegalArgumentException e) {
611                                    // No getter found to retrieve current value
612                                    log.warn("Partial merge only: " + e.getMessage());
613                                    value = null;
614                                }
615                                    catch (Exception e) {
616                                        throw new ServiceException("Could not get property: " + update.toString(), e);
617                                    }
618                                previous = value;
619                            }
620                        }
621                        
622                        // Set new value
623                        try {
624                                if (bean != null && path.length == 1 && dmsFieldNames != null && dmsFieldNames.contains(path[0])) {
625                                        Field field = org.granite.util.Reflections.getField(bean.getClass(), path[0]);
626                                        field.setAccessible(true);
627                                    value = GraniteContext.getCurrentInstance().getGraniteConfig().getConverters().convert(update.getValue(), field.getType());
628                                    // Merge entities into current persistent context if needed
629                                    value = mergeExternal(value, previous);
630                                        Reflections.set(field, bean, value);
631                                }
632                                else if (bean != null) {
633                                    Method setter = Reflections.getSetterMethod(bean.getClass(), path[path.length-1]);
634                                    Type type = setter.getParameterTypes()[0];
635                                        if (bean instanceof Home<?, ?> && "id".equals(path[path.length-1])) {
636                                                // Special (ugly ?) handling for Home object to try to guess id type (setId is of type Object)
637                                                try {
638                                                        Class<?> entityClass = ((Home<?, ?>)bean).getEntityClass();
639                                                        org.granite.util.Entity entity = new org.granite.util.Entity(entityClass);
640                                                        type = entity.getIdentifierType();
641                                                }
642                                                catch (Exception e) {
643                                                        // Ignore
644                                                }
645                                        }
646                                    value = GraniteContext.getCurrentInstance().getGraniteConfig().getConverters().convert(update.getValue(), type);
647                                    // Merge entities into current persistent context if needed
648                                    value = mergeExternal(value, previous);
649                                    Reflections.invoke(setter, bean, value);
650                                }
651                            }
652                            catch (Exception e) {
653                                throw new ServiceException("Could not restore property: " + update.toString(), e);
654                            }
655                    }
656                }
657            }
658            else {
659                // Set context variable
660                if (sourceComponent != null) {
661                    ScopeType scope = sourceComponent.getScope();
662                    if (!((update.getScope() == 2 && (scope == ScopeType.EVENT 
663                                || scope == ScopeType.STATELESS 
664                                || scope == ScopeType.CONVERSATION
665                                || scope == ScopeType.BUSINESS_PROCESS
666                                || scope == ScopeType.METHOD
667                                || scope == ScopeType.PAGE))
668                        || (update.getScope() == 1 && (scope == ScopeType.EVENT
669                                || scope == ScopeType.STATELESS 
670                                || scope == ScopeType.SESSION
671                                || scope == ScopeType.METHOD
672                                || scope == ScopeType.PAGE))
673                        || (update.getScope() == 3 && (scope == ScopeType.EVENT
674                                        || scope == ScopeType.STATELESS
675                                        || scope == ScopeType.METHOD
676                                        || scope == ScopeType.PAGE)))) {
677                        scope = ScopeType.EVENT;
678                    }
679                    
680                    previous = scope.getContext().get(sourceComponentName);
681                    
682                    boolean disabled = previous != null && config.isComponentTideDisabled(sourceComponentName, componentClasses(sourceComponent), previous);
683                    if (!disabled) {
684                        Object value = mergeExternal(update.getValue(), previous);
685                        
686                        scope.getContext().set(sourceComponentName, value);
687                    }
688                }
689                else {
690                    Object[] prev = lookupInStatefulContexts(sourceComponentName, ScopeType.UNSPECIFIED);
691                    ScopeType scope = ScopeType.UNSPECIFIED;
692                    if (prev != null) {
693                        previous = prev[0];
694                        scope = (ScopeType)prev[1];
695                    }
696                    
697                    boolean disabled = previous != null && config.isComponentTideDisabled(sourceComponentName, 
698                                componentClasses(previous), previous);
699                    if (!disabled) {
700                        if (scope == ScopeType.UNSPECIFIED) {
701                            scope = ScopeType.EVENT;
702                            scope.getContext().set(sourceComponentName + "_tide_unspecified_", true);
703                        }
704                        
705                        Object value = mergeExternal(update.getValue(), previous);
706                        
707                        scope.getContext().set(sourceComponentName, value);
708                    }
709                }
710            }
711        }
712    }
713    
714    
715    private static final ScopeType[] EVAL_SCOPE_TYPES = {
716        ScopeType.UNSPECIFIED,
717        ScopeType.EVENT,
718        ScopeType.CONVERSATION,
719        ScopeType.SESSION,
720        ScopeType.BUSINESS_PROCESS,
721        ScopeType.APPLICATION
722    };
723    
724    /**
725     * Evaluate results from context
726     * 
727     * @param component the target component
728     * @param target the target instance
729     * @param nothing used by initializer to avoid interactions with context sync
730     * 
731     * @return list of updates to send back to the client
732     */
733    public List<ContextUpdate> evaluateResults(Component component, Object target, boolean nothing) {
734        
735        List<ContextUpdate> resultsMap = new ArrayList<ContextUpdate>();
736        
737        if (nothing)
738            return resultsMap;
739        
740        List<String> exprs = new ArrayList<String>();
741        GraniteConfig config = GraniteContext.getCurrentInstance().getGraniteConfig();
742        ClassGetter classGetter = GraniteContext.getCurrentInstance().getGraniteConfig().getClassGetter();
743        
744        SeamInitializer.instance();
745        
746        for (ScopeType evalScopeType : EVAL_SCOPE_TYPES) {
747                for (Map.Entry<ContextResult, Boolean> me : getResultsEval(evalScopeType).entrySet()) {
748                    if (!me.getValue())
749                        continue;
750                    
751                    ContextResult res = me.getKey();
752                    Component targetComponent = TideInit.lookupComponent(res.getComponentName());
753                    
754                    String targetComponentName = targetComponent != null ? targetComponent.getName() : res.getComponentName();
755                    
756                    if (res.getExpression() != null && component != null && targetComponent != null && !(component.getName().equals(targetComponentName))) {
757                        // Ignore results concerning other components for this time
758                        // In this case is has to be a non interception enabled component
759                        continue;
760                    }
761                    
762                    int idx = 0;
763                    boolean add = true;
764                    
765                    // Force evaluation of consecutive properties
766                    while (idx >= 0) {
767                        idx = res.getExpression() != null ? res.getExpression().indexOf(".", idx+1) : -1;
768                        
769                        String expr = (idx > 0 ? res.getExpression().substring(0, idx) : res.getExpression());
770                        String ex = expr != null ? res.getComponentName() + "." + expr : res.getComponentName();
771                        
772                        if (!exprs.contains(ex)) {
773                            log.debug("After invocation: evaluating expression #0", ex);
774                            
775                            String[] path = expr != null ? expr.split("\\.") : new String[0];
776                            
777                            try {
778                                Object value = null;
779                                ScopeType scopeType = res instanceof ScopedContextResult ? ((ScopedContextResult)res).getScope() : ScopeType.UNSPECIFIED;
780                                Boolean restrict = res.getRestrict();
781                                
782                            FactoryMethod factoryMethod = null;
783                            FactoryExpression factoryExpression = null;
784                            if (targetComponent == null) {
785                                factoryExpression = TideInit.lookupFactoryExpression(res.getComponentName());
786                                if (factoryExpression == null)
787                                        factoryMethod = TideInit.lookupFactory(res.getComponentName());
788                            }
789                            
790                            if (targetComponent != null) {
791                                if (component != null && component.getName().equals(targetComponent.getName())) {
792                                        value = target;
793                                        scopeType = targetComponent.getScope();
794                                    }
795                                else if (ScopeType.UNSPECIFIED.equals(scopeType)) {
796                                    value = Component.getInstance(targetComponent.getName());
797                                    scopeType = targetComponent.getScope();
798                                    if (ScopeType.STATELESS.equals(scopeType))
799                                        scopeType = ScopeType.EVENT;
800                                    
801                                    if (value != null && config.isComponentTideDisabled(targetComponentName, 
802                                                componentClasses(targetComponent), value))
803                                        add = false;
804                                }
805                                else {
806                                    value = Component.getInstance(targetComponent.getName(), scopeType);
807                                    
808                                    if (value != null && config.isComponentTideDisabled(targetComponentName, 
809                                                componentClasses(value), value))
810                                        add = false;
811                                }
812                                
813                                restrict = targetComponent.beanClassHasAnnotation(Restrict.class);
814                            }
815                            else if (factoryExpression != null) {
816                                String expressionString = factoryExpression.getMethodBinding() != null 
817                                        ? factoryExpression.getMethodBinding().getExpressionString() 
818                                        : factoryExpression.getValueBinding().getExpressionString();
819                                int iedx = expressionString.indexOf(".");
820                                String expressionBaseName = expressionString.substring(2, iedx);
821                                
822                                if (ScopeType.UNSPECIFIED.equals(scopeType)) {
823                                    value = Component.getInstance(res.getComponentName());
824                                    scopeType = factoryExpression.getScope();
825                                    if (ScopeType.STATELESS.equals(scopeType))
826                                        scopeType = ScopeType.EVENT;
827                                    
828                                    if (value != null && config.isComponentTideDisabled(expressionBaseName, componentClasses(value), value))
829                                        add = false;
830                                }
831                                else {
832                                    value = Component.getInstance(res.getComponentName(), scopeType);
833                                    
834                                    if (value != null && config.isComponentTideDisabled(expressionBaseName, componentClasses(value), value))
835                                        add = false;
836                                }
837                                
838                                Component factoryComponent = TideInit.lookupComponent(expressionBaseName);
839                                restrict = factoryComponent != null ? factoryComponent.beanClassHasAnnotation(Restrict.class) : false;
840                            }
841                            else if (factoryMethod != null) {
842                                if (ScopeType.UNSPECIFIED.equals(scopeType)) {
843                                    value = Component.getInstance(factoryMethod.getMethod().getAnnotation(Factory.class).value());
844                                    scopeType = factoryMethod.getScope();
845                                    if (ScopeType.STATELESS.equals(scopeType))
846                                        scopeType = ScopeType.EVENT;
847                                    
848                                    if (value != null && config.isComponentTideDisabled(factoryMethod.getComponent().getName(), 
849                                                componentClasses(factoryMethod.getComponent()), value))
850                                        add = false;
851                                }
852                                else {
853                                    value = Component.getInstance(res.getComponentName(), scopeType);
854                                    
855                                    if (value != null && config.isComponentTideDisabled(factoryMethod.getComponent().getName(), 
856                                                componentClasses(value), value))
857                                        add = false;
858                                }
859
860                                restrict = factoryMethod.getComponent().beanClassHasAnnotation(Restrict.class);
861                            }
862                            else {
863                                Object[] val = lookupInStatefulContexts(res.getComponentName(), scopeType);
864                                if (val != null) {
865                                    value = val[0];
866                                    scopeType = (ScopeType)val[1];
867                                    
868                                    if (value != null && config.isComponentTideDisabled(res.getComponentName(), 
869                                                componentClasses(value), value))
870                                        add = false;
871                                }
872                            }
873                                
874                            if (add) {
875                                Object v0 = null;
876                                String propName = null;
877                                for (int i = 0; i < path.length; i++) {
878                                    if (value == null)
879                                        break;
880                                    // Use modified Reflections for getter because of a bug in Seam 2.0.0
881                                    v0 = value;
882                                    propName = path[i];
883                                    Method getter = null;
884                                    try {
885                                        getter = org.granite.util.Reflections.getGetterMethod(value.getClass(), path[i]);
886                                    }
887                                    catch (IllegalArgumentException e) {
888                                        // GDS-566
889                                    }
890                                    if (getter != null)
891                                        value = Reflections.invoke(getter, value);
892                                }
893                                
894                                        getResultsEval(scopeType).put(res, false);
895                                        
896                                    if (value instanceof TideDataModel) {
897                                        // Unwrap value
898                                        value = ((TideDataModel)value).getWrappedData();
899                                    }
900                                    else if (value != null) {
901                                        if (classGetter != null) {
902                                            classGetter.initialize(v0, propName, value);
903                                            if (res.getExpression() != null) {
904                                                String[] fullPath = res.getExpression().split("\\.");
905                                                Object v = value;
906                                                for (int i = path.length; i < fullPath.length; i++) {
907                                                    // Use modified Reflections for getter because of a bug in Seam 2.0.0
908                                                    Method getter = org.granite.util.Reflections.getGetterMethod(v.getClass(), fullPath[i]);
909                                                    v0 = v;
910                                                    v = Reflections.invoke(getter, v);
911//                                                  if (v == null)
912//                                                      break;
913                                                    classGetter.initialize(v0, fullPath[i], v);
914                                                    if (v == null)
915                                                        break;
916                                                }
917                                            }
918                                        }
919                                    }
920                                
921                                    int scope = (scopeType == ScopeType.CONVERSATION ? 2 : (scopeType == ScopeType.SESSION ? 1 : 3));
922                                    
923                                    resultsMap.add(new ContextUpdate(res.getComponentName(), expr, value, scope, Boolean.TRUE.equals(restrict)));
924                                    add = false;
925                                }
926                                
927                                exprs.add(ex);
928                            }
929                            catch (Exception e) {
930                                throw new ServiceException("Could not evaluate result expression: " + ex, e);
931                            }
932                        }
933                    }
934                    
935                    me.setValue(Boolean.FALSE);
936                }
937        }
938        
939        return resultsMap;
940    }
941    
942    
943    /**
944     * Implementations of intercepted asynchronous calls
945     * Send asynchronous event to client
946     * @param asyncContext current context (session id)
947     * @param targetComponentName target component name
948     * @param methodName method name
949     * @param paramTypes method argument types
950     * @param params argument values
951     * @return result
952     */
953    public Object invokeAsynchronous(AsyncContext asyncContext, String targetComponentName, Class<?> targetComponentClass, String methodName, Class<?>[] paramTypes, Object[] params) {
954        setAsyncContext(asyncContext);
955        
956        // Just another ugly hack: the Seam interceptor has set this variable and we don't want it
957        Contexts.getEventContext().remove("org.jboss.seam.async.AsynchronousIntercepter.REENTRANT");
958        
959        Component component = TideInit.lookupComponent(targetComponentName);
960        
961        // Forces evaluation of all results if they are related to the called component
962        for (Map.Entry<ContextResult, Boolean> me : getResultsEval(component.getScope()).entrySet()) {
963            if (me.getKey().getComponentName().equals(targetComponentName))
964                me.setValue(Boolean.TRUE);
965        }
966        
967        Object target = Component.getInstance(targetComponentName);
968        
969        Method method;
970        try {
971            method = target.getClass().getMethod(methodName, paramTypes);
972        }
973        catch (NoSuchMethodException nsme) {
974           throw new IllegalStateException(nsme);
975        }
976        
977        Object result = Reflections.invokeAndWrap(method, target, params);
978        
979        sendEvent(targetComponentName, targetComponentClass);
980        
981        return result;
982    }
983    
984
985    @Override
986    protected TidePersistenceManager getTidePersistenceManager(boolean create) {
987        return SeamInitializer.instance().getTidePersistenceManager(); 
988    }
989    
990    
991    /**
992     * Search for a named attribute in all contexts, in the
993     * following order: method, event, page, conversation,
994     * session, business process, application.
995     * 
996     * @return the first component found, or null
997     */
998    public static Object[] lookupInStatefulContexts(String name, ScopeType scope) {
999        if ((ScopeType.UNSPECIFIED.equals(scope) || ScopeType.METHOD.equals(scope)) && Contexts.isMethodContextActive()) {
1000            Object result = Contexts.getMethodContext().get(name);
1001            if (result != null)
1002                return new Object[] { result, Contexts.getMethodContext().getType() };
1003        }
1004        
1005        if ((ScopeType.UNSPECIFIED.equals(scope) || ScopeType.EVENT.equals(scope)) && Contexts.isEventContextActive()) {
1006            Object result = Contexts.getEventContext().get(name);
1007            if (result != null)
1008                return new Object[] { result, Contexts.getEventContext().getType() };
1009        }
1010        
1011        if ((ScopeType.UNSPECIFIED.equals(scope) || ScopeType.PAGE.equals(scope)) && Contexts.isPageContextActive()) {
1012            Object result = Contexts.getPageContext().get(name);
1013            if (result != null)
1014                return new Object[] { result, Contexts.getPageContext().getType() };
1015        }
1016        
1017        if ((ScopeType.UNSPECIFIED.equals(scope) || ScopeType.CONVERSATION.equals(scope)) && Contexts.isConversationContextActive()) {
1018            Object result = Contexts.getConversationContext().get(name);
1019            if (result != null)
1020                return new Object[] { result, Contexts.getConversationContext().getType() };
1021        }
1022        
1023        if ((ScopeType.UNSPECIFIED.equals(scope) || ScopeType.SESSION.equals(scope)) && Contexts.isSessionContextActive()) {
1024            Object result = Contexts.getSessionContext().get(name);
1025            if (result != null)
1026                return new Object[] { result, Contexts.getSessionContext().getType() };
1027        }
1028        
1029        if ((ScopeType.UNSPECIFIED.equals(scope) || ScopeType.BUSINESS_PROCESS.equals(scope)) && Contexts.isBusinessProcessContextActive()) {
1030            Object result = Contexts.getBusinessProcessContext().get(name);
1031            if (result != null)
1032                return new Object[] { result, Contexts.getBusinessProcessContext().getType() };
1033        }
1034        
1035        if ((ScopeType.UNSPECIFIED.equals(scope) || ScopeType.APPLICATION.equals(scope)) && Contexts.isApplicationContextActive()) {
1036            Object result = Contexts.getApplicationContext().get(name);
1037            if (result != null)
1038                return new Object[] { result, Contexts.getApplicationContext().getType() };
1039        }
1040        
1041        return ScopeType.UNSPECIFIED.equals(scope) ? null : new Object[] { null, scope };
1042    }
1043}