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