    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}