/** * Copyright (C) 2009 Original Authors * * This file is part of Spring ME. * * Spring ME is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2, or (at your option) any * later version. * * Spring ME is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Spring ME; see the file COPYING. If not, write to the Free * Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * 02110-1301 USA. * * Linking this library statically or dynamically with other modules is * making a combined work based on this library. Thus, the terms and * conditions of the GNU General Public License cover the whole * combination. * * As a special exception, the copyright holders of this library give you * permission to link this library with independent modules to produce an * executable, regardless of the license terms of these independent * modules, and to copy and distribute the resulting executable under * terms of your choice, provided that you also meet, for each linked * independent module, the terms and conditions of the license of that * module. An independent module is a module which is not derived from or * based on this library. If you modify this library, you may extend this * exception to your version of the library, but you are not obligated to * do so. If you do not wish to do so, delete this exception statement * from your version. */ package me.springframework.di.spring; import java.util.ArrayList; import java.util.Collection; import java.util.Iterator; import java.util.List; import me.springframework.di.MapSource; import me.springframework.di.Source; import me.springframework.di.base.AbstractSink; import me.springframework.di.base.MutableConstructorArgument; import me.springframework.di.base.MutableContext; import me.springframework.di.base.MutableInstance; import me.springframework.di.base.MutableInstanceReference; import me.springframework.di.base.MutableListSource; import me.springframework.di.base.MutableMapSource; import me.springframework.di.base.MutablePropertySetter; import com.agilejava.blammo.BlammoLoggerFactory; import com.agilejava.blammo.LoggingKitAdapter; import com.thoughtworks.qdox.JavaDocBuilder; import com.thoughtworks.qdox.model.BeanProperty; import com.thoughtworks.qdox.model.JavaClass; import com.thoughtworks.qdox.model.JavaMethod; import com.thoughtworks.qdox.model.JavaParameter; import com.thoughtworks.qdox.model.Type; /** * {@linkplain Augmentation Augmentation} based on metadata gathered using QDox. * * @author Wilfred Springer (wis) * */ public class QDoxAugmentation implements Augmentation { /** * The object providing access to the sources. */ private JavaDocBuilder builder; /** * The logger, providing data on progress on completing the metadata. */ private Logger logger = (Logger) BlammoLoggerFactory.create(Logger.class); /** * Constructs a new instance, accepting the {@link JavaDocBuilder} providing access to the class * definitions. * * @param builder The object providing access to the metadata we need. */ public QDoxAugmentation(JavaDocBuilder builder) { this.builder = builder; } /** * Provides a way to replace the {@link Logger} with something else. * * @param adapter An adapter, adapting the {@link Logger} interface to some other, lower-level * logging interface. */ public void setLoggingKitAdapter(LoggingKitAdapter adapter) { logger = (Logger) BlammoLoggerFactory.create(Logger.class, adapter); } /** * Completes metadata for the {@link MutableInstance}s passed in. (Just the * {@link MutableInstance}s representing the top-level (root) beans. * * @param context The context containing beans to be augmented. */ public void augment(MutableContext context) { for (MutableInstance instance : context.getInstances().values()) { attribute(instance, context); } } /** * Completes metadata of the {@link MutableInstance} passed in, accepting * <code>allInstances</code> for resolving references. * * @param instance The instance that must be completed. * @param context All beans in the context, for resolving references. */ private void attribute(MutableInstance instance, MutableContext context) { guaranteeType(instance, context); guaranteeTypes(context, instance.getConstructorArguments()); guaranteeTypes(context, instance.getSetters()); logger.logAttributing(instance.getName()); JavaClass cl = builder.getClassByName(instance.getType()); if (isFactoryBean(cl)) { logger.logFoundFactoryBean(instance.getName()); instance.setFactoryBean(true); } if (isInitializingBean(cl)) { instance.setInitMethod("afterPropertiesSet"); } for (MutablePropertySetter setter : instance.getSetters()) { BeanProperty property = cl.getBeanProperty(setter.getName(), true); if (property == null) { logger.logNoSuchProperty(setter.getName(), cl.getName()); } else { setter.setType(property.getType().getValue()); setter.setPrimitive(property.getType().isPrimitive()); attribute(setter.getSource(), context); } } if (instance.getConstructorArguments() != null && instance.getConstructorArguments().size() > 0) { logger.logInformConstructorArguments(instance.getName()); for (MutableConstructorArgument argument : instance.getConstructorArguments()) { attribute(argument.getSource(), context); } List<MutableConstructorArgument> arguments = instance.getConstructorArguments(); JavaMethod method = null; // If there is no factory method if (instance.getFactoryMethod() == null) { logger.logPlainOldConstructor(instance.getName()); method = findConstructor(cl, arguments); if (method == null) { logger.logNoMatchingConstructor(instance); } // If there *is* a factory method, but no factory bean } else if (instance.getFactoryInstance() == null) { logger.logFactoryMethod(instance.getName(), instance.getFactoryMethod()); JavaClass factoryClass = builder.getClassByName(instance.getReferencedType()); method = findMethod(factoryClass, true, instance.getFactoryMethod(), arguments); if (method == null) { logger.logNoMatchingFactoryMethod(instance); } // If there *is* a factory method, *and* a factory bean } else { MutableInstance factoryInstance = context.getByName(instance.getFactoryInstance()); logger.logFactoryBean(instance.getName(), factoryInstance.getName(), instance.getFactoryMethod()); JavaClass factoryClass = builder.getClassByName(factoryInstance.getType()); method = findMethod(factoryClass, false, instance.getFactoryMethod(), arguments); if (method == null) { logger.logNoMatchingFactoryInstanceMethod(instance); } } if (method != null) { copyTypes(method.getParameters(), arguments); } } } private boolean isInitializingBean(JavaClass cl) { return cl.isA("org.springframework.beans.factory.InitializingBean"); } private boolean isFactoryBean(JavaClass cl) { return cl.isA("org.springframework.beans.factory.FactoryBean"); } /** * Guarantee that the type data is available. * * @param instance The MutableInstance for which we need type data. * @param context All beans in the context, for resolving references. */ private void guaranteeType(MutableInstance instance, MutableContext context) { logger.logGuaranteeingTypeKnown(instance.getId(), instance.getType() == null); if (instance.getType() != null) { return; } // In case of no factory method if (instance.getFactoryMethod() == null) { instance.setType(instance.getReferencedType()); } else { // In case of a factory method // In case of a static factory method if (instance.getFactoryInstance() == null) { List<MutableConstructorArgument> arguments = instance.getConstructorArguments(); if (arguments == null) { arguments = new ArrayList<MutableConstructorArgument>(); } JavaClass factoryClass = builder.getClassByName(instance.getReferencedType()); JavaMethod method = findMethod(factoryClass, true, instance.getFactoryMethod(), arguments); instance.setType(method.getReturns().toString()); } else { // If there is a bean factory // Find that factory MutableInstance factoryInstance = context.getByName(instance.getFactoryInstance()); guaranteeType(factoryInstance, context); List<MutableConstructorArgument> arguments = instance.getConstructorArguments(); if (arguments == null) { arguments = new ArrayList<MutableConstructorArgument>(); } JavaClass factoryClass = builder.getClassByName(factoryInstance.getReferencedType()); JavaMethod method = findMethod(factoryClass, false, instance.getFactoryMethod(), arguments); logger.logFoundFactory(instance.getId(), factoryInstance.getId(), instance.getFactoryMethod(), method.getReturns().toString()); instance.setType(method.getReturns().toString()); } } } /** * Ensures that the type of the source is known for every sink. * * @param context The context for resolving references. * @param sinks The sinks for which source types should be resolved. */ private void guaranteeTypes(MutableContext context, Collection<? extends AbstractSink> sinks) { if (sinks == null) { return; } for (AbstractSink sink : sinks) { Source source = sink.getSource(); if (source.getType() == null) { guaranteeType(context, source); } } } /** * Ensures that the type of the given source is known. * * @param context The context for resolving references. * @param sinks The source that should have its type resolved. */ private void guaranteeType(MutableContext context, Source source) { if (source instanceof MutableInstanceReference) { MutableInstanceReference ref = (MutableInstanceReference) source; MutableInstance referent = context.getByName(ref.getName()); if (referent != null) { guaranteeType(referent, context); ref.setType(referent.getType()); ref.setPrimitive(referent.isPrimitive()); } } } /** * Finds a method with the given name on the class passed in, potentially static, wit the given * number of arguments. * * @param cl The class that is supposed to prevent this method. * @param isStatic A boolean indicating if we are searching for a static operation. * @param name The name of the operation. * @param arguments The argument types. * @return A {@link JavaMethod} representing the method found, or <code>null</code> if no such * method was found. */ private JavaMethod findMethod(JavaClass cl, boolean isStatic, String name, List<MutableConstructorArgument> arguments) { for (JavaMethod method : cl.getMethods(true)) { if (name.equals(method.getName()) && method.isStatic() == isStatic && match(arguments, method.getParameters())) { return method; } } return null; } /** * Finds a constructor with the given type of arguments. * * @param cl The {@link JavaClass} that should provide that constructor. * @param arguments The arguments of the constructor. * @return A {@link JavaMethod} representation of that constructor. */ private JavaMethod findConstructor(JavaClass cl, List<MutableConstructorArgument> arguments) { for (JavaMethod method : cl.getMethods()) { if (method.isConstructor()) { logger.logCheckConstructor(method.getDeclarationSignature(true)); if (match(arguments, method.getParameters())) { return method; } } } return null; } /** * Copy type information from an array of {@link JavaParameter}s to a list of * {@link me.springframework.di.base.MutableConstructorArgument}s. (One by one.) * * @param parameters The {@link JavaParameter}s. * @param arguments The {@link MutableConstructorArgument}s. */ private void copyTypes(JavaParameter[] parameters, List<MutableConstructorArgument> arguments) { for (int i = 0; i < parameters.length; i++) { arguments.get(i).setType(parameters[i].getType().getValue()); arguments.get(i).setPrimitive(parameters[i].getType().isPrimitive()); } } /** * Checks if the arguments and parameters match. * * @param arguments The arguments. * @param parameters The parameters. * @return A boolean indicating if they match. */ private boolean match(List<MutableConstructorArgument> arguments, JavaParameter[] parameters) { if (arguments.size() != parameters.length) { logger.logNumberOfArgumentsDontMatch(); return false; } else { for (int i = 0; i < arguments.size(); i++) { if (!match(arguments.get(i), parameters[i])) { return false; } } } return true; } private boolean match(MutableConstructorArgument mutableConstructorArgument, JavaParameter javaParameter) { String argType = mutableConstructorArgument.getType(); if (argType == null) { logger.logConstructorArgumentTypeIsNull(); return false; } if (javaParameter.getType().isResolved()) { if (builder.getClassByName(argType).isA(javaParameter.getType().getJavaClass())) { return true; } else { logger.logConstructorArgumentTypeMismatch(argType, javaParameter.getType()); return false; } } else { logger.logUnresolvedConstructorArgumentType(); return true; } } /** * Completes metadata for the source. * * @param source The {@link Source} for which metadata needs to be completed. * @param context All beans in the context, for resolving references. */ private void attribute(Source source, MutableContext context) { switch (source.getSourceType()) { case Instance: attribute((MutableInstance) source, context); break; case List: attribute((MutableListSource) source, context); break; case Map: attribute((MutableMapSource) source, context); break; case InstanceReference: attribute((MutableInstanceReference) source, context); break; case StringRepresentation: default: } } /** * Completes metadata for the {@link MutableListSource} passed in. * * @param source The {@link MutableListSource} that needs to be completed. * @param allInstances All other {@link MutableInstance}s, for resolving references. */ private void attribute(MutableListSource source, MutableContext context) { for (Source element : source.getElementSources()) { attribute(element, context); } } /** * Completes metadata for the {@link MutableListSource} passed in. * * @param source The {@link MutableListSource} that needs to be completed. * @param context All beans in the context, for resolving references. */ private void attribute(MutableMapSource source, MutableContext context) { for (MapSource.Entry entry : source.getEntries()) { attribute(entry.getKey(), context); attribute(entry.getValue(), context); } } /** * Completes metadata of the {@link MutableInstanceReference} passed in. * * @param reference The {@link MutableInstanceReference} to be completed. * @param context All beans in the context, for resolving references. */ private void attribute(MutableInstanceReference reference, MutableContext context) { MutableInstance referent = context.getByName(reference.getName()); reference.setName(referent.getName()); reference.setReferencedId(referent.getId()); } /** * The logger, logging conditions encountered while completing metadata. * * @blammo.logger */ public interface Logger { /** * The system is starting work to complete the model for specified bean. * * @param name The name the instance being attributed. * @blammo.level debug * @blammo.message Starting work to complete model for bean {name} */ void logAttributing(String name); /** * The system does not have enough type information to be able to judge if the constructor * argument referenced in the Spring configuration matches the one of the constructor it is * inspecting, so it will assume they match. * * @blammo.level warn * @blammo.message Unresolved constructor type; assuming match */ void logUnresolvedConstructorArgumentType(); /** * The system detects incompatibility in two constructor arguments. * * @param expected * @param actual * @blammo.level debug * @blammo.message Constructor argument was expected to be {expected}, but was {actual} */ void logConstructorArgumentTypeMismatch(String expected, Type actual); /** * The system notices that a constructor argument doesn't have its type set. * * @blammo.level error * @blammo.message Constructor argument type is null */ void logConstructorArgumentTypeIsNull(); /** * The system checked a constructor, but finds that the number of arguments doesn't match * the number expected. * * @blammo.level debug * @blammo.message Number of arguments does not meet expectations */ void logNumberOfArgumentsDontMatch(); /** * The system is checking if this constructor may be the one. * * @param callSignature The signature of the constructor. * @blammo.level debug * @blammo.message Checking constructor "{callSignature}" */ void logCheckConstructor(String callSignature); /** * The system detects that the factory set for instance does not have the required factory * method. * * @param instance The instance that needs to created. * @blammo.message No corresponding factory method on the factory defined for {instance}. * @blammo.level error */ void logNoMatchingFactoryInstanceMethod(MutableInstance instance); /** * The system detects that there is no corresponding factory method for this instance. * * @param instance The instance that needs to be created. * @blammo.message No corresponding factory method for {instance}. * @blammo.level error */ void logNoMatchingFactoryMethod(MutableInstance instance); /** * The system detects that there is no corresponding constructor for this instance. * * @param instance The instance for which we need a constructor. * @blammo.message No corresponding constructor for {instance}. * @blammo.level error */ void logNoMatchingConstructor(MutableInstance instance); /** * The system notices that a property does not exist. * * @param propertyName The name of the property missing. * @param className The class on which this property should have been defined. * @blammo.message Class {className} does not have a property called {propertyName} * @blammo.level error */ void logNoSuchProperty(String propertyName, String className); /** * The system found the factory method for the given instance. * * @param instance * @param factoryInstance * @param factoryMethod * @param type * @blammo.level debug * @blammo.message Creating bean {instance} using #{factoryMethod}() on instance * {factoryInstance}, returning type {type} */ void logFoundFactory(String instance, String factoryInstance, String factoryMethod, String type); /** * The system verifies if the type is known for the given bean. * * @param id The identifier of the bean. * @blammo.level debug * @blammo.message Check type known for bean {id}: {known} */ void logGuaranteeingTypeKnown(String id, boolean known); /** * The system trying to find a constructor. * * @param size The number of constructor argument defined. * @param match A boolean, indicating if it matches the current constructor. * @blammo.level debug * @blammo.message Checking a constructor, to see if the number of arguments is {size}: * {match} */ void logComparingArguments(int size, boolean match); /** * The system is informing that the specified bean will be constructed using a factory * method defined on another bean. * * @param name The name of the bean that will be constructed. * @param factoryInstance The name of the bean that will construct the instance. * @param factoryMethod The factory method on that bean. * @blammo.level debug * @blammo.message Bean {name} will be constructed using factory method {factoryMethod} on * {factoryInstance} */ void logFactoryBean(String name, String factoryInstance, String factoryMethod); /** * The system is informing that the specified bean will be constructed using the factory * method defined on the class itself. * * @param name The name of the bean. * @param factoryMethod The (static) factory method defined to construct the instance. * @blammo.level debug * @blammo.message Bean {name} will be constructed using the factory method {factoryMethod} */ void logFactoryMethod(String name, String factoryMethod); /** * The system is informing that the specified bean will be constructed using a plain old * constructor. * * @param name The name of the bean being created using a plain old constructor. * @blammo.level debug * @blammo.message Bean {name} will be constructed using its own constructor */ void logPlainOldConstructor(String name); /** * The system is informing that the specified bean is configured by constructor arguments. * * @param name The name of the instance currently processed. * @blammo.level debug * @blammo.message Bean {name} will be configured using constructor argument settings */ void logInformConstructorArguments(String name); /** * The system is informing that the specificied bean implements FactoryBean. * * @param name The name of the bean. * @blammo.level debug * @blammo.message Bean {name} is a FactoryBean */ void logFoundFactoryBean(String name); } }