// Copyright 2006-2014 The Apache Software Foundation // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package org.apache.tapestry5.ioc.internal; import org.apache.tapestry5.func.F; import org.apache.tapestry5.func.Mapper; import org.apache.tapestry5.func.Predicate; import org.apache.tapestry5.ioc.*; import org.apache.tapestry5.ioc.annotations.*; import org.apache.tapestry5.ioc.def.*; import org.apache.tapestry5.ioc.internal.util.CollectionFactory; import org.apache.tapestry5.ioc.internal.util.InternalUtils; import org.apache.tapestry5.ioc.services.PlasticProxyFactory; import org.slf4j.Logger; import java.lang.annotation.Annotation; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.Iterator; import java.util.Map; import java.util.Set; /** * Starting from the Class for a module, identifies all the services (service builder methods), * decorators (service * decorator methods) and (not yet implemented) contributions (service contributor methods). */ public class DefaultModuleDefImpl implements ModuleDef2, ServiceDefAccumulator { /** * The prefix used to identify service builder methods. */ private static final String BUILD_METHOD_NAME_PREFIX = "build"; /** * The prefix used to identify service decorator methods. */ private static final String DECORATE_METHOD_NAME_PREFIX = "decorate"; /** * The prefix used to identify service contribution methods. */ private static final String CONTRIBUTE_METHOD_NAME_PREFIX = "contribute"; private static final String ADVISE_METHOD_NAME_PREFIX = "advise"; private final static Map<Class, ConfigurationType> PARAMETER_TYPE_TO_CONFIGURATION_TYPE = CollectionFactory .newMap(); private final Class moduleClass; private final Logger logger; private final PlasticProxyFactory proxyFactory; /** * Keyed on service id. */ private final Map<String, ServiceDef> serviceDefs = CollectionFactory.newCaseInsensitiveMap(); /** * Keyed on decorator id. */ private final Map<String, DecoratorDef> decoratorDefs = CollectionFactory.newCaseInsensitiveMap(); private final Map<String, AdvisorDef> advisorDefs = CollectionFactory.newCaseInsensitiveMap(); private final Set<ContributionDef> contributionDefs = CollectionFactory.newSet(); private final Set<Class> defaultMarkers = CollectionFactory.newSet(); private final Set<StartupDef> startups = CollectionFactory.newSet(); private final static Set<Method> OBJECT_METHODS = CollectionFactory.newSet(Object.class.getMethods()); static { PARAMETER_TYPE_TO_CONFIGURATION_TYPE.put(Configuration.class, ConfigurationType.UNORDERED); PARAMETER_TYPE_TO_CONFIGURATION_TYPE.put(OrderedConfiguration.class, ConfigurationType.ORDERED); PARAMETER_TYPE_TO_CONFIGURATION_TYPE.put(MappedConfiguration.class, ConfigurationType.MAPPED); } /** * @param moduleClass * the class that is responsible for building services, etc. * @param logger * based on the class name of the module * @param proxyFactory * factory used to create proxy classes at runtime */ public DefaultModuleDefImpl(Class<?> moduleClass, Logger logger, PlasticProxyFactory proxyFactory) { this.moduleClass = moduleClass; this.logger = logger; this.proxyFactory = proxyFactory; Marker annotation = moduleClass.getAnnotation(Marker.class); if (annotation != null) { InternalUtils.validateMarkerAnnotations(annotation.value()); defaultMarkers.addAll(Arrays.asList(annotation.value())); } // Want to verify that every public method is meaningful to Tapestry IoC. Remaining methods // might // have typos, i.e., "createFoo" that should be "buildFoo". Set<Method> methods = CollectionFactory.newSet(moduleClass.getMethods()); Iterator<Method> methodIterator = methods.iterator(); while (methodIterator.hasNext()) { Method method = methodIterator.next(); for (Method objectMethod : OBJECT_METHODS) { if (signaturesAreEqual(method, objectMethod)) { methodIterator.remove(); break; } } } removeSyntheticMethods(methods); boolean modulePreventsServiceDecoration = moduleClass.getAnnotation(PreventServiceDecoration.class) != null; grind(methods, modulePreventsServiceDecoration); bind(methods, modulePreventsServiceDecoration); if (methods.isEmpty()) return; throw new RuntimeException(String.format("Module class %s contains unrecognized public methods: %s.", moduleClass.getName(), InternalUtils.joinSorted(methods))); } private static boolean signaturesAreEqual(Method m1, Method m2) { if (m1.getName() == m2.getName()) { if (!m1.getReturnType().equals(m2.getReturnType())) return false; Class<?>[] params1 = m1.getParameterTypes(); Class<?>[] params2 = m2.getParameterTypes(); if (params1.length == params2.length) { for (int i = 0; i < params1.length; i++) { if (params1[i] != params2[i]) return false; } return true; } } return false; } /** * Identifies the module class and a list of service ids within the module. */ @Override public String toString() { return String.format("ModuleDef[%s %s]", moduleClass.getName(), InternalUtils.joinSorted(serviceDefs.keySet())); } @Override public Class getBuilderClass() { return moduleClass; } @Override public Set<String> getServiceIds() { return serviceDefs.keySet(); } @Override public ServiceDef getServiceDef(String serviceId) { return serviceDefs.get(serviceId); } private void removeSyntheticMethods(Set<Method> methods) { Iterator<Method> iterator = methods.iterator(); while (iterator.hasNext()) { Method m = iterator.next(); if (m.isSynthetic() || m.getName().startsWith("$")) { iterator.remove(); } } } private void grind(Set<Method> remainingMethods, boolean modulePreventsServiceDecoration) { Method[] methods = moduleClass.getMethods(); Comparator<Method> c = new Comparator<Method>() { // By name, ascending, then by parameter count, descending. @Override public int compare(Method o1, Method o2) { int result = o1.getName().compareTo(o2.getName()); if (result == 0) result = o2.getParameterTypes().length - o1.getParameterTypes().length; return result; } }; Arrays.sort(methods, c); for (Method m : methods) { String name = m.getName(); if (name.startsWith(BUILD_METHOD_NAME_PREFIX)) { addServiceDef(m, modulePreventsServiceDecoration); remainingMethods.remove(m); continue; } if (name.startsWith(DECORATE_METHOD_NAME_PREFIX) || m.isAnnotationPresent(Decorate.class)) { addDecoratorDef(m); remainingMethods.remove(m); continue; } if (name.startsWith(CONTRIBUTE_METHOD_NAME_PREFIX) || m.isAnnotationPresent(Contribute.class)) { addContributionDef(m); remainingMethods.remove(m); continue; } if (name.startsWith(ADVISE_METHOD_NAME_PREFIX) || m.isAnnotationPresent(Advise.class)) { addAdvisorDef(m); remainingMethods.remove(m); continue; } if (m.isAnnotationPresent(Startup.class)) { addStartupDef(m); remainingMethods.remove(m); continue; } } } private void addStartupDef(Method method) { startups.add(new StartupDefImpl(method)); } private void addContributionDef(Method method) { Contribute annotation = method.getAnnotation(Contribute.class); Class serviceInterface = annotation == null ? null : annotation.value(); String serviceId = annotation != null ? null : stripMethodPrefix(method, CONTRIBUTE_METHOD_NAME_PREFIX); Class returnType = method.getReturnType(); if (!returnType.equals(void.class)) logger.warn(IOCMessages.contributionWrongReturnType(method)); ConfigurationType type = null; for (Class parameterType : method.getParameterTypes()) { ConfigurationType thisParameter = PARAMETER_TYPE_TO_CONFIGURATION_TYPE.get(parameterType); if (thisParameter != null) { if (type != null) throw new RuntimeException(IOCMessages.tooManyContributionParameters(method)); type = thisParameter; } } if (type == null) throw new RuntimeException(IOCMessages.noContributionParameter(method)); Set<Class> markers = extractMarkers(method, Contribute.class, Optional.class); boolean optional = method.getAnnotation(Optional.class) != null; ContributionDef3 def = new ContributionDefImpl(serviceId, method, optional, proxyFactory, serviceInterface, markers); contributionDefs.add(def); } private void addDecoratorDef(Method method) { Decorate annotation = method.getAnnotation(Decorate.class); Class serviceInterface = annotation == null ? null : annotation.serviceInterface(); // TODO: methods just named "decorate" String decoratorId = annotation == null ? stripMethodPrefix(method, DECORATE_METHOD_NAME_PREFIX) : extractId( serviceInterface, annotation.id()); // TODO: Check for duplicates Class returnType = method.getReturnType(); if (returnType.isPrimitive() || returnType.isArray()) { throw new RuntimeException(String.format( "Method %s is named like a service decorator method, but the return type (%s) is not acceptable (try Object).", InternalUtils.asString(method), method.getReturnType().getCanonicalName())); } Set<Class> markers = extractMarkers(method, Decorate.class); DecoratorDef def = new DecoratorDefImpl(method, extractPatterns(decoratorId, method), extractConstraints(method), proxyFactory, decoratorId, serviceInterface, markers); decoratorDefs.put(decoratorId, def); } private <T extends Annotation> String[] extractPatterns(String id, Method method) { Match match = method.getAnnotation(Match.class); if (match == null) { return new String[]{id}; } return match.value(); } private String[] extractConstraints(Method method) { Order order = method.getAnnotation(Order.class); if (order == null) return null; return order.value(); } private void addAdvisorDef(Method method) { Advise annotation = method.getAnnotation(Advise.class); Class serviceInterface = annotation == null ? null : annotation.serviceInterface(); // TODO: methods just named "decorate" String advisorId = annotation == null ? stripMethodPrefix(method, ADVISE_METHOD_NAME_PREFIX) : extractId( serviceInterface, annotation.id()); // TODO: Check for duplicates Class returnType = method.getReturnType(); if (!returnType.equals(void.class)) throw new RuntimeException(String.format("Advise method %s does not return void.", toString(method))); boolean found = false; for (Class pt : method.getParameterTypes()) { if (pt.equals(MethodAdviceReceiver.class)) { found = true; break; } } if (!found) throw new RuntimeException(String.format("Advise method %s must take a parameter of type %s.", toString(method), MethodAdviceReceiver.class.getName())); Set<Class> markers = extractMarkers(method, Advise.class); AdvisorDef def = new AdvisorDefImpl(method, extractPatterns(advisorId, method), extractConstraints(method), proxyFactory, advisorId, serviceInterface, markers); advisorDefs.put(advisorId, def); } private String extractId(Class serviceInterface, String id) { return InternalUtils.isBlank(id) ? serviceInterface.getSimpleName() : id; } private String toString(Method method) { return InternalUtils.asString(method, proxyFactory); } private String stripMethodPrefix(Method method, String prefix) { return method.getName().substring(prefix.length()); } /** * Invoked for public methods that have the proper prefix. */ private void addServiceDef(final Method method, boolean modulePreventsServiceDecoration) { String serviceId = InternalUtils.getServiceId(method); if (serviceId == null) { serviceId = stripMethodPrefix(method, BUILD_METHOD_NAME_PREFIX); } // If the method name was just "build()", then work from the return type. if (serviceId.equals("")) serviceId = method.getReturnType().getSimpleName(); // Any number of parameters is fine, we'll adapt. Eventually we have to check // that we can satisfy the parameters requested. Thrown exceptions of the method // will be caught and wrapped, so we don't need to check those. But we do need a proper // return type. Class returnType = method.getReturnType(); if (returnType.isPrimitive() || returnType.isArray()) throw new RuntimeException( String.format("Method %s is named like a service builder method, but the return type (%s) is not acceptable (try an interface).", InternalUtils.asString(method), method.getReturnType().getCanonicalName())); String scope = extractServiceScope(method); boolean eagerLoad = method.isAnnotationPresent(EagerLoad.class); boolean preventDecoration = modulePreventsServiceDecoration || method.getAnnotation(PreventServiceDecoration.class) != null; ObjectCreatorSource source = new ObjectCreatorSource() { @Override public ObjectCreator constructCreator(ServiceBuilderResources resources) { return new ServiceBuilderMethodInvoker(resources, getDescription(), method); } @Override public String getDescription() { return DefaultModuleDefImpl.this.toString(method); } }; Set<Class> markers = CollectionFactory.newSet(defaultMarkers); markers.addAll(extractServiceDefMarkers(method)); ServiceDefImpl serviceDef = new ServiceDefImpl(returnType, null, serviceId, markers, scope, eagerLoad, preventDecoration, source); addServiceDef(serviceDef); } private Collection<Class> extractServiceDefMarkers(Method method) { Marker annotation = method.getAnnotation(Marker.class); if (annotation == null) return Collections.emptyList(); return CollectionFactory.newList(annotation.value()); } @SuppressWarnings("rawtypes") private Set<Class> extractMarkers(Method method, final Class... annotationClassesToSkip) { return F.flow(method.getAnnotations()).map(new Mapper<Annotation, Class>() { @Override public Class map(Annotation value) { return value.annotationType(); } }).filter(new Predicate<Class>() { @Override public boolean accept(Class element) { for (Class skip : annotationClassesToSkip) { if (skip.equals(element)) { return false; } } return true; } }).toSet(); } @Override public void addServiceDef(ServiceDef serviceDef) { String serviceId = serviceDef.getServiceId(); ServiceDef existing = serviceDefs.get(serviceId); if (existing != null) throw new RuntimeException(IOCMessages.buildMethodConflict(serviceId, serviceDef.toString(), existing.toString())); serviceDefs.put(serviceId, serviceDef); } private String extractServiceScope(Method method) { Scope scope = method.getAnnotation(Scope.class); return scope != null ? scope.value() : ScopeConstants.DEFAULT; } @Override public Set<DecoratorDef> getDecoratorDefs() { return toSet(decoratorDefs); } @Override public Set<ContributionDef> getContributionDefs() { return contributionDefs; } @Override public String getLoggerName() { return moduleClass.getName(); } /** * See if the build class defined a bind method and invoke it. * * @param remainingMethods * set of methods as yet unaccounted for * @param modulePreventsServiceDecoration * true if {@link org.apache.tapestry5.ioc.annotations.PreventServiceDecoration} on * module * class */ private void bind(Set<Method> remainingMethods, boolean modulePreventsServiceDecoration) { Throwable failure; Method bindMethod = null; try { bindMethod = moduleClass.getMethod("bind", ServiceBinder.class); if (!Modifier.isStatic(bindMethod.getModifiers())) throw new RuntimeException(IOCMessages.bindMethodMustBeStatic(toString(bindMethod))); ServiceBinderImpl binder = new ServiceBinderImpl(this, bindMethod, proxyFactory, defaultMarkers, modulePreventsServiceDecoration); bindMethod.invoke(null, binder); binder.finish(); remainingMethods.remove(bindMethod); return; } catch (NoSuchMethodException ex) { // No problem! Many modules will not have such a method. return; } catch (IllegalArgumentException ex) { failure = ex; } catch (IllegalAccessException ex) { failure = ex; } catch (InvocationTargetException ex) { failure = ex.getTargetException(); } String methodId = toString(bindMethod); throw new RuntimeException(IOCMessages.errorInBindMethod(methodId, failure), failure); } @Override public Set<AdvisorDef> getAdvisorDefs() { return toSet(advisorDefs); } private <K, V> Set<V> toSet(Map<K, V> map) { return CollectionFactory.newSet(map.values()); } @Override public Set<StartupDef> getStartups() { return startups; } }