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    }