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