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
015package org.apache.tapestry5.ioc.internal;
016
017import org.apache.tapestry5.func.F;
018import org.apache.tapestry5.func.Mapper;
019import org.apache.tapestry5.func.Predicate;
020import org.apache.tapestry5.ioc.*;
021import org.apache.tapestry5.ioc.annotations.*;
022import org.apache.tapestry5.ioc.def.*;
023import org.apache.tapestry5.ioc.internal.util.CollectionFactory;
024import org.apache.tapestry5.ioc.internal.util.InternalUtils;
025import org.apache.tapestry5.ioc.services.PlasticProxyFactory;
026import org.slf4j.Logger;
027
028import java.lang.annotation.Annotation;
029import java.lang.reflect.InvocationTargetException;
030import java.lang.reflect.Method;
031import java.lang.reflect.Modifier;
032import java.util.*;
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 */
039public 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}