blob: 430fe9c1d506cd0f28b8950334fc6d65ebe215c1 [file] [log] [blame]
// Copyright 2006, 2007, 2008, 2009, 2010 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 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;
import org.apache.tapestry5.func.F;
import org.apache.tapestry5.func.Mapper;
import org.apache.tapestry5.func.Predicate;
import org.apache.tapestry5.ioc.AdvisorDef;
import org.apache.tapestry5.ioc.Configuration;
import org.apache.tapestry5.ioc.MappedConfiguration;
import org.apache.tapestry5.ioc.MethodAdviceReceiver;
import org.apache.tapestry5.ioc.ObjectCreator;
import org.apache.tapestry5.ioc.OrderedConfiguration;
import org.apache.tapestry5.ioc.ScopeConstants;
import org.apache.tapestry5.ioc.ServiceBinder;
import org.apache.tapestry5.ioc.ServiceBuilderResources;
import org.apache.tapestry5.ioc.annotations.Advise;
import org.apache.tapestry5.ioc.annotations.Contribute;
import org.apache.tapestry5.ioc.annotations.Decorate;
import org.apache.tapestry5.ioc.annotations.EagerLoad;
import org.apache.tapestry5.ioc.annotations.Marker;
import org.apache.tapestry5.ioc.annotations.Match;
import org.apache.tapestry5.ioc.annotations.Order;
import org.apache.tapestry5.ioc.annotations.PreventServiceDecoration;
import org.apache.tapestry5.ioc.annotations.Scope;
import org.apache.tapestry5.ioc.annotations.ServiceId;
import org.apache.tapestry5.ioc.annotations.Startup;
import org.apache.tapestry5.ioc.def.ContributionDef;
import org.apache.tapestry5.ioc.def.ContributionDef2;
import org.apache.tapestry5.ioc.def.DecoratorDef;
import org.apache.tapestry5.ioc.def.ModuleDef2;
import org.apache.tapestry5.ioc.def.ServiceDef;
import org.apache.tapestry5.ioc.internal.util.CollectionFactory;
import org.apache.tapestry5.ioc.internal.util.InternalUtils;
import org.apache.tapestry5.ioc.services.ClassFactory;
import org.slf4j.Logger;
/**
* 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 ClassFactory classFactory;
/**
* 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 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 classFactory
* factory used to create new classes at runtime or locate method line numbers for
* error
* reporting
*/
public DefaultModuleDefImpl(Class<?> moduleClass, Logger logger, ClassFactory classFactory)
{
this.moduleClass = moduleClass;
this.logger = logger;
this.classFactory = classFactory;
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());
methods.removeAll(OBJECT_METHODS);
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)));
}
/**
* 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()));
}
public Class getBuilderClass()
{
return moduleClass;
}
public Set<String> getServiceIds()
{
return serviceDefs.keySet();
}
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.
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)
{
Set<Class> markers = Collections.emptySet();
ContributionDef2 def = new ContributionDefImpl("RegistryStartup", method, classFactory, Runnable.class, markers);
contributionDefs.add(def);
}
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);
ContributionDef2 def = new ContributionDefImpl(serviceId, method, classFactory, 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(IOCMessages.decoratorMethodWrongReturnType(method));
Set<Class> markers = extractMarkers(method, Decorate.class);
DecoratorDef def = new DecoratorDefImpl(method, extractPatterns(annotation, decoratorId, method),
extractConstraints(method), classFactory, decoratorId, serviceInterface, markers);
decoratorDefs.put(decoratorId, def);
}
private <T extends Annotation> String[] extractPatterns(T annotation, String id, Method method)
{
if(annotation != null)
return new String[]{};
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(annotation, advisorId, method), extractConstraints(method),
classFactory, 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, classFactory);
}
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)
{
ServiceId serviceIdAnnotation = method.getAnnotation(ServiceId.class);
String serviceId;
if (serviceIdAnnotation != null)
{
serviceId = serviceIdAnnotation.value();
}
else
{
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(IOCMessages.buildMethodWrongReturnType(method));
String scope = extractServiceScope(method);
boolean eagerLoad = method.isAnnotationPresent(EagerLoad.class);
boolean preventDecoration = modulePreventsServiceDecoration
|| method.getAnnotation(PreventServiceDecoration.class) != null;
ObjectCreatorSource source = new ObjectCreatorSource()
{
public ObjectCreator constructCreator(ServiceBuilderResources resources)
{
return new ServiceBuilderMethodInvoker(resources, getDescription(), method);
}
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 annotationClassToSkip)
{
return F.flow(method.getAnnotations()).map(new Mapper<Annotation, Class>()
{
public Class map(Annotation value)
{
return value.annotationType();
};
}).filter(new Predicate<Class>()
{
public boolean accept(Class object)
{
return !object.equals(annotationClassToSkip);
}
}).toSet();
}
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;
}
public Set<DecoratorDef> getDecoratorDefs()
{
return toSet(decoratorDefs);
}
public Set<ContributionDef> getContributionDefs()
{
return contributionDefs;
}
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, classFactory, 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);
}
public Set<AdvisorDef> getAdvisorDefs()
{
return toSet(advisorDefs);
}
private <K, V> Set<V> toSet(Map<K, V> map)
{
return CollectionFactory.newSet(map.values());
}
}