    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}