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