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