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