001// Copyright 2006-2014 The Apache Software Foundation
002//
003// Licensed under the Apache License, Version 2.0 (the "License");
004// you may not use this file except in compliance with the License.
005// You may obtain a copy of the License at
006//
007// http://www.apache.org/licenses/LICENSE-2.0
008//
009// Unless required by applicable law or agreed to in writing, software
010// distributed under the License is distributed on an "AS IS" BASIS,
011// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
012// See the License for the specific language governing permissions and
013// limitations under the License.
014
015package org.apache.tapestry5.ioc.internal;
016
017import java.lang.annotation.Annotation;
018import java.lang.reflect.InvocationTargetException;
019import java.lang.reflect.Method;
020import java.lang.reflect.Modifier;
021import java.util.Arrays;
022import java.util.Collection;
023import java.util.Collections;
024import java.util.Comparator;
025import java.util.Iterator;
026import java.util.Map;
027import java.util.Set;
028
029import org.apache.tapestry5.commons.Configuration;
030import org.apache.tapestry5.commons.MappedConfiguration;
031import org.apache.tapestry5.commons.ObjectCreator;
032import org.apache.tapestry5.commons.OrderedConfiguration;
033import org.apache.tapestry5.commons.internal.util.TapestryException;
034import org.apache.tapestry5.commons.services.PlasticProxyFactory;
035import org.apache.tapestry5.commons.util.CollectionFactory;
036import org.apache.tapestry5.func.F;
037import org.apache.tapestry5.func.Mapper;
038import org.apache.tapestry5.func.Predicate;
039import org.apache.tapestry5.ioc.AdvisorDef;
040import org.apache.tapestry5.ioc.MethodAdviceReceiver;
041import org.apache.tapestry5.ioc.ScopeConstants;
042import org.apache.tapestry5.ioc.ServiceBinder;
043import org.apache.tapestry5.ioc.ServiceBuilderResources;
044import org.apache.tapestry5.ioc.annotations.Advise;
045import org.apache.tapestry5.ioc.annotations.Contribute;
046import org.apache.tapestry5.ioc.annotations.Decorate;
047import org.apache.tapestry5.ioc.annotations.EagerLoad;
048import org.apache.tapestry5.ioc.annotations.Marker;
049import org.apache.tapestry5.ioc.annotations.Match;
050import org.apache.tapestry5.ioc.annotations.Optional;
051import org.apache.tapestry5.ioc.annotations.Order;
052import org.apache.tapestry5.ioc.annotations.PreventServiceDecoration;
053import org.apache.tapestry5.ioc.annotations.Scope;
054import org.apache.tapestry5.ioc.annotations.Startup;
055import org.apache.tapestry5.ioc.def.ContributionDef;
056import org.apache.tapestry5.ioc.def.ContributionDef3;
057import org.apache.tapestry5.ioc.def.DecoratorDef;
058import org.apache.tapestry5.ioc.def.ModuleDef2;
059import org.apache.tapestry5.ioc.def.ServiceDef;
060import org.apache.tapestry5.ioc.def.StartupDef;
061import org.apache.tapestry5.ioc.internal.util.InternalUtils;
062import org.slf4j.Logger;
063
064/**
065 * Starting from the Class for a module, identifies all the services (service builder methods),
066 * decorators (service
067 * decorator methods) and (not yet implemented) contributions (service contributor methods).
068 */
069public class DefaultModuleDefImpl implements ModuleDef2, ServiceDefAccumulator
070{
071    /**
072     * The prefix used to identify service builder methods.
073     */
074    private static final String BUILD_METHOD_NAME_PREFIX = "build";
075
076    /**
077     * The prefix used to identify service decorator methods.
078     */
079    private static final String DECORATE_METHOD_NAME_PREFIX = "decorate";
080
081    /**
082     * The prefix used to identify service contribution methods.
083     */
084    private static final String CONTRIBUTE_METHOD_NAME_PREFIX = "contribute";
085
086    private static final String ADVISE_METHOD_NAME_PREFIX = "advise";
087
088    private final static Map<Class, ConfigurationType> PARAMETER_TYPE_TO_CONFIGURATION_TYPE = CollectionFactory
089            .newMap();
090
091    private final Class moduleClass;
092
093    private final Logger logger;
094
095    private final PlasticProxyFactory proxyFactory;
096
097    /**
098     * Keyed on service id.
099     */
100    private final Map<String, ServiceDef> serviceDefs = CollectionFactory.newCaseInsensitiveMap();
101
102    /**
103     * Keyed on decorator id.
104     */
105    private final Map<String, DecoratorDef> decoratorDefs = CollectionFactory.newCaseInsensitiveMap();
106
107    private final Map<String, AdvisorDef> advisorDefs = CollectionFactory.newCaseInsensitiveMap();
108
109    private final Set<ContributionDef> contributionDefs = CollectionFactory.newSet();
110
111    private final Set<Class> defaultMarkers = CollectionFactory.newSet();
112
113    private final Set<StartupDef> startups = CollectionFactory.newSet();
114
115    private final static Set<Method> OBJECT_METHODS = CollectionFactory.newSet(Object.class.getMethods());
116
117    static
118    {
119        PARAMETER_TYPE_TO_CONFIGURATION_TYPE.put(Configuration.class, ConfigurationType.UNORDERED);
120        PARAMETER_TYPE_TO_CONFIGURATION_TYPE.put(OrderedConfiguration.class, ConfigurationType.ORDERED);
121        PARAMETER_TYPE_TO_CONFIGURATION_TYPE.put(MappedConfiguration.class, ConfigurationType.MAPPED);
122    }
123
124    /**
125     * @param moduleClass
126     *         the class that is responsible for building services, etc.
127     * @param logger
128     *         based on the class name of the module
129     * @param proxyFactory
130     *         factory used to create proxy classes at runtime
131     */
132    public DefaultModuleDefImpl(Class<?> moduleClass, Logger logger, PlasticProxyFactory proxyFactory)
133    {
134        this.moduleClass = moduleClass;
135        this.logger = logger;
136        this.proxyFactory = proxyFactory;
137
138        Marker annotation = moduleClass.getAnnotation(Marker.class);
139
140        if (annotation != null)
141        {
142            InternalUtils.validateMarkerAnnotations(annotation.value());
143            defaultMarkers.addAll(Arrays.asList(annotation.value()));
144        }
145
146        // Want to verify that every public method is meaningful to Tapestry IoC. Remaining methods
147        // might
148        // have typos, i.e., "createFoo" that should be "buildFoo".
149
150        Set<Method> methods;
151        try 
152        {
153            methods = CollectionFactory.newSet(moduleClass.getMethods());
154        }
155        catch (Exception e)
156        {
157            throw new TapestryException(
158                    "Exception while processing module class " + moduleClass.getName() +
159                    ": " + e.getMessage(), e);
160        }
161
162        Iterator<Method> methodIterator = methods.iterator();
163
164        while (methodIterator.hasNext())
165        {
166            Method method = methodIterator.next();
167            for (Method objectMethod : OBJECT_METHODS)
168            {
169                if (signaturesAreEqual(method, objectMethod))
170                {
171                    methodIterator.remove();
172                    break;
173                }
174            }
175        }
176
177        removeSyntheticMethods(methods);
178        removeGroovyObjectMethods(methods);
179
180        boolean modulePreventsServiceDecoration = moduleClass.getAnnotation(PreventServiceDecoration.class) != null;
181
182        grind(methods, modulePreventsServiceDecoration);
183        bind(methods, modulePreventsServiceDecoration);
184
185        if (methods.isEmpty())
186            return;
187
188        throw new RuntimeException(String.format("Module class %s contains unrecognized public methods: %s.",
189                moduleClass.getName(), InternalUtils.joinSorted(methods)));
190    }
191
192    private static boolean signaturesAreEqual(Method m1, Method m2)
193    {
194        if (m1.getName() == m2.getName()) {
195            if (!m1.getReturnType().equals(m2.getReturnType()))
196                return false;
197            Class<?>[] params1 = m1.getParameterTypes();
198            Class<?>[] params2 = m2.getParameterTypes();
199            if (params1.length == params2.length)
200            {
201                for (int i = 0; i < params1.length; i++) {
202                    if (params1[i] != params2[i])
203                        return false;
204                }
205                return true;
206            }
207        }
208        return false;
209    }
210
211    /**
212     * Identifies the module class and a list of service ids within the module.
213     */
214    @Override
215    public String toString()
216    {
217        return String.format("ModuleDef[%s %s]", moduleClass.getName(), InternalUtils.joinSorted(serviceDefs.keySet()));
218    }
219
220    @Override
221    public Class getBuilderClass()
222    {
223        return moduleClass;
224    }
225
226    @Override
227    public Set<String> getServiceIds()
228    {
229        return serviceDefs.keySet();
230    }
231
232    @Override
233    public ServiceDef getServiceDef(String serviceId)
234    {
235        return serviceDefs.get(serviceId);
236    }
237    
238    private void removeGroovyObjectMethods(Set<Method> methods)
239    {
240        Iterator<Method> iterator = methods.iterator();
241
242        while (iterator.hasNext())
243        {
244            Method m = iterator.next();
245            final String name = m.getName();
246
247            if (m.getDeclaringClass().getName().equals("groovy.lang.GroovyObject")
248                    || (name.equals("getMetaClass") && m.getReturnType().getName().equals("groovy.lang.MetaClass"))
249                || (m.getParameterCount() == 1 && m.getParameterTypes()[0].getName().equals("groovy.lang.MetaClass")))
250            {
251                iterator.remove();
252            }
253        }
254    }
255
256    private void removeSyntheticMethods(Set<Method> methods)
257    {
258        Iterator<Method> iterator = methods.iterator();
259
260        while (iterator.hasNext())
261        {
262            Method m = iterator.next();
263
264            if (m.isSynthetic() || m.getName().startsWith("$"))
265            {
266                iterator.remove();
267            }
268        }
269    }
270
271    private void grind(Set<Method> remainingMethods, boolean modulePreventsServiceDecoration)
272    {
273        Method[] methods = moduleClass.getMethods();
274
275        Comparator<Method> c = new Comparator<Method>()
276        {
277            // By name, ascending, then by parameter count, descending.
278
279            @Override
280            public int compare(Method o1, Method o2)
281            {
282                int result = o1.getName().compareTo(o2.getName());
283
284                if (result == 0)
285                    result = o2.getParameterTypes().length - o1.getParameterTypes().length;
286
287                return result;
288            }
289        };
290
291        Arrays.sort(methods, c);
292
293        for (Method m : methods)
294        {
295            String name = m.getName();
296
297            if (name.startsWith(BUILD_METHOD_NAME_PREFIX))
298            {
299                addServiceDef(m, modulePreventsServiceDecoration);
300                remainingMethods.remove(m);
301                continue;
302            }
303
304            if (name.startsWith(DECORATE_METHOD_NAME_PREFIX) || m.isAnnotationPresent(Decorate.class))
305            {
306                addDecoratorDef(m);
307                remainingMethods.remove(m);
308                continue;
309            }
310
311            if (name.startsWith(CONTRIBUTE_METHOD_NAME_PREFIX) || m.isAnnotationPresent(Contribute.class))
312            {
313                addContributionDef(m);
314                remainingMethods.remove(m);
315                continue;
316            }
317
318            if (name.startsWith(ADVISE_METHOD_NAME_PREFIX) || m.isAnnotationPresent(Advise.class))
319            {
320                addAdvisorDef(m);
321                remainingMethods.remove(m);
322                continue;
323            }
324
325            if (m.isAnnotationPresent(Startup.class))
326            {
327                addStartupDef(m);
328                remainingMethods.remove(m);
329                continue;
330            }
331        }
332    }
333
334    private void addStartupDef(Method method)
335    {
336        startups.add(new StartupDefImpl(method));
337    }
338
339    private void addContributionDef(Method method)
340    {
341        Contribute annotation = method.getAnnotation(Contribute.class);
342
343        Class serviceInterface = annotation == null ? null : annotation.value();
344
345        String serviceId = annotation != null ? null : stripMethodPrefix(method, CONTRIBUTE_METHOD_NAME_PREFIX);
346
347        Class returnType = method.getReturnType();
348        if (!returnType.equals(void.class))
349            logger.warn(IOCMessages.contributionWrongReturnType(method));
350
351        ConfigurationType type = null;
352
353        for (Class parameterType : method.getParameterTypes())
354        {
355            ConfigurationType thisParameter = PARAMETER_TYPE_TO_CONFIGURATION_TYPE.get(parameterType);
356
357            if (thisParameter != null)
358            {
359                if (type != null)
360                    throw new RuntimeException(IOCMessages.tooManyContributionParameters(method));
361
362                type = thisParameter;
363            }
364        }
365
366        if (type == null)
367            throw new RuntimeException(IOCMessages.noContributionParameter(method));
368
369        Set<Class> markers = extractMarkers(method, Contribute.class, Optional.class);
370
371        boolean optional = method.getAnnotation(Optional.class) != null;
372
373        ContributionDef3 def = new ContributionDefImpl(serviceId, method, optional, proxyFactory, serviceInterface, markers);
374
375        contributionDefs.add(def);
376    }
377
378    private void addDecoratorDef(Method method)
379    {
380        Decorate annotation = method.getAnnotation(Decorate.class);
381
382        Class serviceInterface = annotation == null ? null : annotation.serviceInterface();
383
384        // TODO: methods just named "decorate"
385
386        String decoratorId = annotation == null ? stripMethodPrefix(method, DECORATE_METHOD_NAME_PREFIX) : extractId(
387                serviceInterface, annotation.id());
388
389        // TODO: Check for duplicates
390
391        Class returnType = method.getReturnType();
392
393        if (returnType.isPrimitive() || returnType.isArray())
394        {
395            throw new RuntimeException(String.format(
396                    "Method %s is named like a service decorator method, but the return type (%s) is not acceptable (try Object).",
397                    InternalUtils.asString(method),
398                    method.getReturnType().getCanonicalName()));
399        }
400
401
402        Set<Class> markers = extractMarkers(method, Decorate.class);
403
404        DecoratorDef def = new DecoratorDefImpl(method, extractPatterns(decoratorId, method),
405                extractConstraints(method), proxyFactory, decoratorId, serviceInterface, markers);
406
407        decoratorDefs.put(decoratorId, def);
408    }
409
410    private <T extends Annotation> String[] extractPatterns(String id, Method method)
411    {
412        Match match = method.getAnnotation(Match.class);
413
414        if (match == null)
415        {
416            return new String[]{id};
417        }
418
419        return match.value();
420    }
421
422    private String[] extractConstraints(Method method)
423    {
424        Order order = method.getAnnotation(Order.class);
425
426        if (order == null)
427            return null;
428
429        return order.value();
430    }
431
432    private void addAdvisorDef(Method method)
433    {
434        Advise annotation = method.getAnnotation(Advise.class);
435
436        Class serviceInterface = annotation == null ? null : annotation.serviceInterface();
437
438        // TODO: methods just named "decorate"
439
440        String advisorId = annotation == null ? stripMethodPrefix(method, ADVISE_METHOD_NAME_PREFIX) : extractId(
441                serviceInterface, annotation.id());
442
443        // TODO: Check for duplicates
444
445        Class returnType = method.getReturnType();
446
447        if (!returnType.equals(void.class))
448            throw new RuntimeException(String.format("Advise method %s does not return void.", toString(method)));
449
450        boolean found = false;
451
452        for (Class pt : method.getParameterTypes())
453        {
454            if (pt.equals(MethodAdviceReceiver.class))
455            {
456                found = true;
457
458                break;
459            }
460        }
461
462        if (!found)
463            throw new RuntimeException(String.format("Advise method %s must take a parameter of type %s.",
464                    toString(method), MethodAdviceReceiver.class.getName()));
465
466        Set<Class> markers = extractMarkers(method, Advise.class);
467
468        AdvisorDef def = new AdvisorDefImpl(method, extractPatterns(advisorId, method),
469                extractConstraints(method), proxyFactory, advisorId, serviceInterface, markers);
470
471        advisorDefs.put(advisorId, def);
472
473    }
474
475    private String extractId(Class serviceInterface, String id)
476    {
477        return InternalUtils.isBlank(id) ? serviceInterface.getSimpleName() : id;
478    }
479
480    private String toString(Method method)
481    {
482        return InternalUtils.asString(method, proxyFactory);
483    }
484
485    private String stripMethodPrefix(Method method, String prefix)
486    {
487        return method.getName().substring(prefix.length());
488    }
489
490    /**
491     * Invoked for public methods that have the proper prefix.
492     */
493    private void addServiceDef(final Method method, boolean modulePreventsServiceDecoration)
494    {
495        String serviceId = InternalUtils.getServiceId(method);
496
497        if (serviceId == null)
498        {
499            serviceId = stripMethodPrefix(method, BUILD_METHOD_NAME_PREFIX);
500        }
501
502        // If the method name was just "build()", then work from the return type.
503
504        if (serviceId.equals(""))
505            serviceId = method.getReturnType().getSimpleName();
506
507        // Any number of parameters is fine, we'll adapt. Eventually we have to check
508        // that we can satisfy the parameters requested. Thrown exceptions of the method
509        // will be caught and wrapped, so we don't need to check those. But we do need a proper
510        // return type.
511
512        Class returnType = method.getReturnType();
513
514        if (returnType.isPrimitive() || returnType.isArray())
515            throw new RuntimeException(
516                    String.format("Method %s is named like a service builder method, but the return type (%s) is not acceptable (try an interface).",
517                            InternalUtils.asString(method),
518                            method.getReturnType().getCanonicalName()));
519
520        String scope = extractServiceScope(method);
521        boolean eagerLoad = method.isAnnotationPresent(EagerLoad.class);
522
523        boolean preventDecoration = modulePreventsServiceDecoration
524                || method.getAnnotation(PreventServiceDecoration.class) != null;
525
526        ObjectCreatorSource source = new ObjectCreatorSource()
527        {
528            @Override
529            public ObjectCreator constructCreator(ServiceBuilderResources resources)
530            {
531                return new ServiceBuilderMethodInvoker(resources, getDescription(), method);
532            }
533
534            @Override
535            public String getDescription()
536            {
537                return DefaultModuleDefImpl.this.toString(method);
538            }
539        };
540
541        Set<Class> markers = CollectionFactory.newSet(defaultMarkers);
542        markers.addAll(extractServiceDefMarkers(method));
543
544        ServiceDefImpl serviceDef = new ServiceDefImpl(returnType, null, serviceId, markers, scope, eagerLoad,
545                preventDecoration, source);
546
547        addServiceDef(serviceDef);
548    }
549
550    private Collection<Class> extractServiceDefMarkers(Method method)
551    {
552        Marker annotation = method.getAnnotation(Marker.class);
553
554        if (annotation == null)
555            return Collections.emptyList();
556
557        return CollectionFactory.newList(annotation.value());
558    }
559
560    @SuppressWarnings("rawtypes")
561    private Set<Class> extractMarkers(Method method, final Class... annotationClassesToSkip)
562    {
563        return F.flow(method.getAnnotations()).map(new Mapper<Annotation, Class>()
564        {
565            @Override
566            public Class map(Annotation value)
567            {
568                return value.annotationType();
569            }
570        }).filter(new Predicate<Class>()
571        {
572            @Override
573            public boolean accept(Class element)
574            {
575                for (Class skip : annotationClassesToSkip)
576                {
577                    if (skip.equals(element))
578                    {
579                        return false;
580                    }
581                }
582
583                return true;
584            }
585        }).toSet();
586    }
587
588    @Override
589    public void addServiceDef(ServiceDef serviceDef)
590    {
591        String serviceId = serviceDef.getServiceId();
592
593        ServiceDef existing = serviceDefs.get(serviceId);
594
595        if (existing != null)
596            throw new RuntimeException(IOCMessages.buildMethodConflict(serviceId, serviceDef.toString(),
597                    existing.toString()));
598
599        serviceDefs.put(serviceId, serviceDef);
600    }
601
602    private String extractServiceScope(Method method)
603    {
604        Scope scope = method.getAnnotation(Scope.class);
605
606        return scope != null ? scope.value() : ScopeConstants.DEFAULT;
607    }
608
609    @Override
610    public Set<DecoratorDef> getDecoratorDefs()
611    {
612        return toSet(decoratorDefs);
613    }
614
615    @Override
616    public Set<ContributionDef> getContributionDefs()
617    {
618        return contributionDefs;
619    }
620
621    @Override
622    public String getLoggerName()
623    {
624        return moduleClass.getName();
625    }
626
627    /**
628     * See if the build class defined a bind method and invoke it.
629     *
630     * @param remainingMethods
631     *         set of methods as yet unaccounted for
632     * @param modulePreventsServiceDecoration
633     *         true if {@link org.apache.tapestry5.ioc.annotations.PreventServiceDecoration} on
634     *         module
635     *         class
636     */
637    private void bind(Set<Method> remainingMethods, boolean modulePreventsServiceDecoration)
638    {
639        Throwable failure;
640        Method bindMethod = null;
641
642        try
643        {
644            bindMethod = moduleClass.getMethod("bind", ServiceBinder.class);
645
646            if (!Modifier.isStatic(bindMethod.getModifiers()))
647                throw new RuntimeException(IOCMessages.bindMethodMustBeStatic(toString(bindMethod)));
648
649            ServiceBinderImpl binder = new ServiceBinderImpl(this, bindMethod, proxyFactory, defaultMarkers,
650                    modulePreventsServiceDecoration);
651
652            bindMethod.invoke(null, binder);
653
654            binder.finish();
655
656            remainingMethods.remove(bindMethod);
657
658            return;
659        } catch (NoSuchMethodException ex)
660        {
661            // No problem! Many modules will not have such a method.
662
663            return;
664        } catch (IllegalArgumentException ex)
665        {
666            failure = ex;
667        } catch (IllegalAccessException ex)
668        {
669            failure = ex;
670        } catch (InvocationTargetException ex)
671        {
672            failure = ex.getTargetException();
673        }
674
675        String methodId = toString(bindMethod);
676
677        throw new RuntimeException(IOCMessages.errorInBindMethod(methodId, failure), failure);
678    }
679
680    @Override
681    public Set<AdvisorDef> getAdvisorDefs()
682    {
683        return toSet(advisorDefs);
684    }
685
686    private <K, V> Set<V> toSet(Map<K, V> map)
687    {
688        return CollectionFactory.newSet(map.values());
689    }
690
691    @Override
692    public Set<StartupDef> getStartups()
693    {
694        return startups;
695    }
696}