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