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