001/*
002  GRANITE DATA SERVICES
003  Copyright (C) 2011 GRANITE DATA SERVICES S.A.S.
004
005  This file is part of Granite Data Services.
006
007  Granite Data Services is free software; you can redistribute it and/or modify
008  it under the terms of the GNU Library General Public License as published by
009  the Free Software Foundation; either version 2 of the License, or (at your
010  option) any later version.
011
012  Granite Data Services is distributed in the hope that it will be useful, but
013  WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
014  FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License
015  for more details.
016
017  You should have received a copy of the GNU Library General Public License
018  along with this library; if not, see <http://www.gnu.org/licenses/>.
019*/
020
021package org.granite.tide.seam;
022
023import static org.jboss.seam.annotations.Install.FRAMEWORK;
024
025import java.lang.reflect.Field;
026import java.lang.reflect.Method;
027import java.util.HashMap;
028import java.util.HashSet;
029import java.util.Iterator;
030import java.util.List;
031import java.util.Map;
032import java.util.Set;
033
034import org.granite.tide.annotations.BypassTideInterceptor;
035import org.granite.tide.async.AsyncPublisher;
036import org.granite.tide.data.DataEnabled;
037import org.granite.tide.seam.async.TideAsynchronousInterceptor;
038import org.jboss.seam.Component;
039import org.jboss.seam.Namespace;
040import org.jboss.seam.ScopeType;
041import org.jboss.seam.annotations.Install;
042import org.jboss.seam.annotations.Logger;
043import org.jboss.seam.annotations.Name;
044import org.jboss.seam.annotations.Scope;
045import org.jboss.seam.annotations.Startup;
046import org.jboss.seam.annotations.intercept.BypassInterceptors;
047import org.jboss.seam.annotations.intercept.InterceptorType;
048import org.jboss.seam.async.AsynchronousInterceptor;
049import org.jboss.seam.contexts.Context;
050import org.jboss.seam.contexts.Contexts;
051import org.jboss.seam.core.Init;
052import org.jboss.seam.core.Init.FactoryExpression;
053import org.jboss.seam.core.Init.FactoryMethod;
054import org.jboss.seam.init.Initialization;
055import org.jboss.seam.intercept.Interceptor;
056import org.jboss.seam.log.Log;
057import org.jboss.seam.util.Reflections;
058
059
060/**
061 * Particular initializer for Tide which instruments all Seam components
062 * - Tide bijection/invocation interceptor is added
063 * - Tide asynchronous interceptor is added
064 * 
065 * @author William DRAI
066 */
067@Scope(ScopeType.APPLICATION)
068@Name("org.granite.tide.seam.init")
069@Install(precedence=FRAMEWORK)
070@Startup
071@BypassInterceptors
072public class TideInit {
073        
074        @Logger
075        private Log log;
076        
077        private Map<Component, Set<FactoryVariable>> factoryComponents = new HashMap<Component, Set<FactoryVariable>>();
078        
079    
080    public TideInit() {
081        Context context = Contexts.getApplicationContext();
082        for (String name : context.getNames()) {
083            if (!name.endsWith(Initialization.COMPONENT_SUFFIX))
084                continue;
085            Object object = context.get(name);
086            if (object instanceof Component) {
087                Component component = (Component) object;
088                if (component.isInterceptionEnabled() && component.getScope() != ScopeType.APPLICATION)
089                    instrumentComponent(component);
090            }
091        }
092        
093        Init.instance().importNamespace("org.granite.tide");
094        
095        scanFactoryComponents();
096    }
097    
098    
099    public static Set<FactoryVariable> getFactoredVariables(Component component) {
100        TideInit init = (TideInit)Contexts.getApplicationContext().get(TideInit.class);         
101        return init.factoryComponents.get(component);
102    }
103    
104    
105    @SuppressWarnings("unchecked")
106    private void scanFactoryComponents() {
107        try {
108                Init init = Init.instance();
109                
110                Field field = init.getClass().getDeclaredField("factories");
111                field.setAccessible(true);
112                Map<String, FactoryMethod> factories = (Map<String, FactoryMethod>)field.get(init);         
113                for (Map.Entry<String, FactoryMethod> factory : factories.entrySet()) {
114                        String componentName = factory.getValue().getComponent().getName();
115                        addFactoryVariable(componentName, factory.getKey(), factory.getValue().getScope());
116                }
117                
118                field = init.getClass().getDeclaredField("factoryMethodExpressions");
119                field.setAccessible(true);
120                Map<String, FactoryExpression> factoryExpressions = (Map<String, FactoryExpression>)field.get(init);                
121                for (Map.Entry<String, FactoryExpression> factoryExpression : factoryExpressions.entrySet()) {
122                        String expressionString = factoryExpression.getValue().getMethodBinding().getExpressionString();
123                        String componentName = resolveComponentName(expressionString);
124                        if (componentName != null)
125                                addFactoryVariable(componentName, factoryExpression.getKey(), factoryExpression.getValue().getScope());
126                }
127                
128                field = init.getClass().getDeclaredField("factoryValueExpressions");
129                field.setAccessible(true);
130                factoryExpressions = (Map<String, FactoryExpression>)field.get(init);             
131                for (Map.Entry<String, FactoryExpression> factoryExpression : factoryExpressions.entrySet()) {
132                        String expressionString = factoryExpression.getValue().getMethodBinding().getExpressionString();
133                        String componentName = resolveComponentName(expressionString);
134                        if (componentName != null)
135                                addFactoryVariable(componentName, factoryExpression.getKey(), factoryExpression.getValue().getScope());
136                }
137        }
138        catch (Exception e) {
139                log.error("Could not initialize factory components map correctly", e);
140        }
141    }
142    
143    
144    private String resolveComponentName(String expressionString) {
145        Init init = Init.instance();
146        
147        String expr = expressionString.startsWith("#{") ? expressionString.substring(2, expressionString.length()-1) : expressionString;
148                int idx = expr.indexOf('.');
149                String componentName = idx >= 0 ? expr.substring(0, idx) : expr;
150                Component component = lookupComponent(componentName);
151                if (component != null)
152                        return componentName;
153        
154                if (idx < 0)
155                        return null;
156                
157        Namespace ns = init.getRootNamespace().getChild(componentName);         
158        while (ns != null) {
159                expr = expr.substring(idx+1);
160                idx = expr.indexOf('.');
161                String name = idx >= 0 ? expr.substring(0, idx) : expr;
162                
163                if (ns.hasChild(name))
164                        ns = ns.getChild(name);
165                else {
166                        try {
167                                // Must use this hack to get the right name as all methods are private in Seam Namespace object
168                                Method m = ns.getClass().getDeclaredMethod("qualifyName", String.class);
169                                m.setAccessible(true);
170                                componentName = (String)Reflections.invoke(m, ns, name);
171                                component = Component.forName(componentName);
172                                return component != null ? componentName : null;
173                        }
174                        catch (Exception e) {
175                                // Ignore
176                        }
177                }
178        }
179        
180        return null;
181    }
182    
183    
184    private void addFactoryVariable(String componentName, String variableName, ScopeType scope) {
185        Component component = lookupComponent(componentName);
186                Set<FactoryVariable> variables = factoryComponents.get(component);
187                if (variables == null) {
188                        variables = new HashSet<FactoryVariable>();
189                        factoryComponents.put(component, variables);
190                }
191                variables.add(new FactoryVariable(variableName, scope));
192    }
193    
194    
195    public static final class FactoryVariable {
196        private final String variableName;
197        private final ScopeType scope;
198        
199        public FactoryVariable(String variableName, ScopeType scope) {
200                this.variableName = variableName;
201                this.scope = scope;
202        }
203        
204        public String getVariableName() {
205                return variableName;
206        }
207        
208        public ScopeType getScope() {
209                return scope;
210        }
211        
212        @Override
213        public boolean equals(Object obj) {
214                if (!(obj instanceof FactoryVariable))
215                        return false;
216                return ((FactoryVariable)obj).variableName.equals(variableName) && ((FactoryVariable)obj).scope.equals(scope);    
217        }
218        
219        @Override
220        public int hashCode() {
221                return (variableName + "@" + scope).hashCode();
222        }
223    }
224    
225    
226    /**
227     * Implementation of component lookup for Seam
228     * Uses a hack to handle imports and fully qualified names
229     * 
230     * @param componentName name of the component
231     * @return Component object
232     */
233    static Component lookupComponent(String componentName) {
234        Init init = Init.instance();
235        
236        Component component = Component.forName(componentName);
237        if (component != null)
238                return component;
239        
240        // Look for the component in the imported namespaces (always give precedence to org.granite.tide overriden components)
241        for (Namespace ns : init.getGlobalImports()) {
242            try {
243                // Must use this hack to get the right name as all methods are private in Seam Namespace object
244                Method m = ns.getClass().getDeclaredMethod("qualifyName", String.class);
245                m.setAccessible(true);
246                String nsName = (String)Reflections.invoke(m, ns, componentName);
247                if (!nsName.startsWith("org.granite.tide"))
248                        continue;
249                component = Component.forName(nsName);
250                if (component != null)
251                        return component;
252            }
253            catch (Exception e) {
254                // Ignore
255            }
256        }
257        
258        for (Namespace ns : init.getGlobalImports()) {
259                try {
260                        // Must use this hack to get the right name as all methods are private in Seam Namespace object
261                        Method m = ns.getClass().getDeclaredMethod("qualifyName", String.class);
262                        m.setAccessible(true);
263                        String nsName = (String)Reflections.invoke(m, ns, componentName);
264                        component = Component.forName(nsName);
265                        if (component != null)
266                                return component;
267                }
268                catch (Exception e) {
269                        // Ignore
270                }
271        }
272        return null;
273    }
274    
275    
276    /**
277     * Implementation of factory lookup for Seam
278     * Uses a hack to manage imports and fully qualified names
279     * 
280     * @param componentName name of the factored component
281     * @return FactoryMethod object
282     */
283    static FactoryMethod lookupFactory(String componentName) {
284        Init init = Init.instance();
285        
286        FactoryMethod factoryMethod = init.getFactory(componentName);
287        if (factoryMethod != null)
288                return factoryMethod;
289        
290        // Look for the factory in the imported namespaces (always give precedence to org.granite.tide overriden components)
291        for (Namespace ns : init.getGlobalImports()) {
292                try {
293                        // Must use this hack to get the right name as all methods are private in Seam Namespace object
294                        Method m = ns.getClass().getDeclaredMethod("qualifyName", String.class);
295                        m.setAccessible(true);
296                        String nsName = (String)Reflections.invoke(m, ns, componentName);
297                        if (!nsName.startsWith("org.granite.tide"))
298                                continue;
299                        factoryMethod = init.getFactory(nsName);
300                        if (factoryMethod != null)
301                                return factoryMethod;
302                }
303                catch (Exception e) {
304                        // Ignore
305                }
306        }
307        
308        for (Namespace ns : init.getGlobalImports()) {
309                try {
310                        // Must use this hack to get the right name as all methods are private in Seam Namespace object
311                        Method m = ns.getClass().getDeclaredMethod("qualifyName", String.class);
312                        m.setAccessible(true);
313                        String nsName = (String)Reflections.invoke(m, ns, componentName);
314                        factoryMethod = init.getFactory(nsName);
315                        if (factoryMethod != null)
316                                return factoryMethod;
317                }
318                catch (Exception e) {
319                        // Ignore
320                }
321        }
322        
323        return null;
324    }
325    
326    /**
327     * Implementation of factory expression lookup for Seam
328     * Uses a hack to manage imports and fully qualified names
329     * 
330     * @param componentName name of the factored component
331     * @return FactoryMethod object
332     */
333    static FactoryExpression lookupFactoryExpression(String componentName) {
334        Init init = Init.instance();
335        
336        FactoryExpression factoryExpression = init.getFactoryMethodExpression(componentName);
337        if (factoryExpression != null)
338                return factoryExpression;
339        factoryExpression = init.getFactoryValueExpression(componentName);
340        if (factoryExpression != null)
341                return factoryExpression;
342        
343        // Look for the factory expression in the imported namespaces (always give precedence to org.granite.tide overriden components)
344        for (Namespace ns : init.getGlobalImports()) {
345                try {
346                        // Must use this hack to get the right name as all methods are private in Seam Namespace object
347                        Method m = ns.getClass().getDeclaredMethod("qualifyName", String.class);
348                        m.setAccessible(true);
349                        String nsName = (String)Reflections.invoke(m, ns, componentName);
350                        if (!nsName.startsWith("org.granite.tide"))
351                                continue;
352                        
353                factoryExpression = init.getFactoryMethodExpression(nsName);
354                if (factoryExpression != null)
355                        return factoryExpression;
356                factoryExpression = init.getFactoryValueExpression(nsName);
357                if (factoryExpression != null)
358                        return factoryExpression;
359                }
360                catch (Exception e) {
361                        // Ignore
362                }
363        }
364        for (Namespace ns : init.getGlobalImports()) {
365                try {
366                        // Must use this hack to get the right name as all methods are private in Seam Namespace object
367                        Method m = ns.getClass().getDeclaredMethod("qualifyName", String.class);
368                        m.setAccessible(true);
369                        String nsName = (String)Reflections.invoke(m, ns, componentName);
370                        
371                factoryExpression = init.getFactoryMethodExpression(nsName);
372                if (factoryExpression != null)
373                        return factoryExpression;
374                factoryExpression = init.getFactoryValueExpression(nsName);
375                if (factoryExpression != null)
376                        return factoryExpression;
377                }
378                catch (Exception e) {
379                        // Ignore
380                }
381        }
382        
383        return null;
384    }
385    
386    
387    private void instrumentComponent(Component component) {
388        if (component.getBeanClass().isAnnotationPresent(BypassTideInterceptor.class))
389                return;
390        
391        List<Interceptor> li = component.getInterceptors(InterceptorType.ANY);
392        
393        boolean newSortServer = false, newSortClient = false;
394                
395        boolean found = false;
396        for (Interceptor i : li) {
397            if (i.getUserInterceptorClass().equals(TideInterceptor.class)) {
398                found = true;
399                break;
400            }
401        }
402        if (!found) {
403            component.addInterceptor(new Interceptor(new TideInterceptor(), component));
404            newSortServer = true;
405        }
406        
407        if (component.beanClassHasAnnotation(DataEnabled.class) && component.getBeanClass().getAnnotation(DataEnabled.class).useInterceptor()) {
408                found = false;
409                for (Interceptor i : li) {
410                    if (i.getUserInterceptorClass().equals(TideDataPublishingInterceptor.class)) {
411                        found = true;
412                        break;
413                    }
414                }
415                if (!found) {
416                    component.addInterceptor(new Interceptor(new TideDataPublishingInterceptor(), component));
417                    newSortServer = true;
418                }
419        }
420 
421        // Check if AsyncPublisher installed
422        AsyncPublisher asyncPublisher = (AsyncPublisher)Component.getInstance("org.granite.tide.seam.async.publisher");
423        if (asyncPublisher != null) {
424            boolean async = false;
425            li = component.getClientSideInterceptors();
426            for (Iterator<Interceptor> i = li.iterator(); i.hasNext(); ) {
427                Interceptor in = i.next();
428                if (in.getUserInterceptorClass().equals(AsynchronousInterceptor.class)) {
429                    async = true;
430                    break;
431                }
432            }
433            if (async) {
434                component.addInterceptor(new Interceptor(new TideAsynchronousInterceptor(), component));
435                newSortClient = true;
436            }
437        }
438        
439        if (newSortServer || newSortClient) {
440            // Force correct sorting of interceptors: hack because newSort is private
441            try {
442                Method m = component.getClass().getDeclaredMethod("newSort", List.class);
443                m.setAccessible(true);
444                if (newSortServer)
445                    m.invoke(component, component.getInterceptors(InterceptorType.SERVER));
446                if (newSortClient)
447                    m.invoke(component, component.getInterceptors(InterceptorType.CLIENT));
448            }
449            catch (Exception e) {
450                // Ignore all
451            }
452        }
453    }
454}