cross.applicationContext.ReflectionApplicationContextGenerator.java Source code

Java tutorial

Introduction

Here is the source code for cross.applicationContext.ReflectionApplicationContextGenerator.java

Source

/*
 * Cross, common runtime object support system.
 * Copyright (C) 2008-2012, The authors of Cross. All rights reserved.
 *
 * Project website: http://maltcms.sf.net
 *
 * Cross may be used under the terms of either the
 *
 * GNU Lesser General Public License (LGPL)
 * http://www.gnu.org/licenses/lgpl.html
 *
 * or the
 *
 * Eclipse Public License (EPL)
 * http://www.eclipse.org/org/documents/epl-v10.php
 *
 * As a user/recipient of Cross, you may choose which license to receive the code
 * under. Certain files or entire directories may not be covered by this
 * dual license, but are subject to licenses compatible to both LGPL and EPL.
 * License exceptions are explicitly declared in all relevant files or in a
 * LICENSE file in the relevant directories.
 *
 * Cross 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. Please consult the relevant license documentation
 * for details.
 */
package cross.applicationContext;

import cross.annotations.AnnotationInspector;
import cross.commands.fragments.AFragmentCommand;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import lombok.extern.slf4j.Slf4j;
import org.openide.util.Lookup;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider;
import org.springframework.core.type.filter.AssignableTypeFilter;
import org.w3c.dom.Comment;
import org.w3c.dom.Document;
import org.w3c.dom.Element;

/**
 * This code is based on
 * https://github.com/mangstadt/Spring-Application-Context-Generator but uses
 * the compiled class information via reflection and the java service
 * provider/netbeans lookup API in order to resolve references. Values are
 * populated with their default values from the instantiated objects.
 *
 * The Generator will accept any number of concrete or abstract fully qualified
 * class names and will introspect the associated classes. Abstract classes will
 * be used to query for corresponding service implementations. Classes must have
 * a default no-args constructor in order for the service loader facility to
 * work properly, so there are no constructor-arg elements created at the
 * moment.
 *
 * @author Nils Hoffmann
 */
@Slf4j
public class ReflectionApplicationContextGenerator {

    /**
     * Creates a stub application context xml file for the given classes. The
     * default bean scope is 'prototype'.
     *
     * @param outputFile the output file for the generated xml file
     * @param springVersion the spring version to use for validation
     * @param defaultScope the default scope
     * @param classes the classes to generate stubs for
     */
    public static void createContextXml(File outputFile, String springVersion, String defaultScope,
            String... classes) {
        //generate the application context XML
        ReflectionApplicationContextGenerator generator = new ReflectionApplicationContextGenerator(springVersion,
                defaultScope);
        for (String className : classes) {
            try {
                Collection<?> serviceImplementations = Lookup.getDefault().lookupAll(Class.forName(className));
                if (serviceImplementations.isEmpty()) {
                    //standard bean, add it
                    try {
                        generator.addBean(className);
                    } catch (ClassNotFoundException ex) {
                        Logger.getLogger(ReflectionApplicationContextGenerator.class.getName()).log(Level.SEVERE,
                                null, ex);
                    }
                } else {//service provider implementation
                    for (Object obj : serviceImplementations) {
                        try {
                            generator.addBean(obj.getClass().getCanonicalName());
                        } catch (ClassNotFoundException ex) {
                            Logger.getLogger(ReflectionApplicationContextGenerator.class.getName())
                                    .log(Level.SEVERE, null, ex);
                        }
                    }
                }
            } catch (ClassNotFoundException ex) {
                Logger.getLogger(ReflectionApplicationContextGenerator.class.getName()).log(Level.SEVERE, null, ex);
            }

        }
        Document document = generator.getDocument();
        Element element = document.getDocumentElement();
        Comment comment = document.createComment(
                "In order to use this template in a runnable pipeline, please include it from another application context xml file.");
        element.getParentNode().insertBefore(comment, element);
        writeToFile(document, outputFile);
    }

    /**
     * Write the given xml document to the outputFile.
     *
     * @param document the xml document
     * @param outputFile the output file
     */
    public static void writeToFile(Document document, File outputFile) {
        try {
            TransformerFactory transfac = TransformerFactory.newInstance();
            transfac.setAttribute("indent-number", 2);
            Transformer trans = transfac.newTransformer();
            trans.setOutputProperty(OutputKeys.INDENT, "yes");
            DOMSource domSource = new DOMSource(document);
            BufferedOutputStream bw = null;
            try {
                outputFile.getParentFile().mkdirs();
                log.info("Writing to file {}", outputFile.getAbsolutePath());
                bw = new BufferedOutputStream(new FileOutputStream(outputFile));
                trans.transform(domSource, new StreamResult(new OutputStreamWriter(bw, "utf-8")));
                bw.close();
            } catch (IOException ex) {
                Logger.getLogger(ReflectionApplicationContextGenerator.class.getName()).log(Level.SEVERE, null, ex);
            } finally {
                if (bw != null) {
                    try {
                        bw.close();
                    } catch (IOException ex) {
                        Logger.getLogger(ReflectionApplicationContextGenerator.class.getName()).log(Level.SEVERE,
                                null, ex);
                    }
                }
            }

            //System.out.println(xmlString);
        } catch (TransformerException ex) {
            Logger.getLogger(ReflectionApplicationContextGenerator.class.getName()).log(Level.SEVERE, null, ex);
        }
    }

    /**
     *
     * @param args
     */
    public static void main(String[] args) {
        String[] classes = args;
        String springVersion = "3.0";
        String scope = "prototype";
        //generate the application context XML
        ReflectionApplicationContextGenerator generator = new ReflectionApplicationContextGenerator(springVersion,
                scope);
        for (String className : classes) {
            try {
                Collection<?> serviceImplementations = Lookup.getDefault().lookupAll(Class.forName(className));
                if (serviceImplementations.isEmpty()) {
                    //standard bean, add it
                    try {
                        generator.addBean(className);
                    } catch (ClassNotFoundException ex) {
                        log.warn("Could not find class", ex);
                    }
                } else {//service provider implementation
                    for (Object obj : serviceImplementations) {
                        try {
                            generator.addBean(obj.getClass().getCanonicalName());
                        } catch (ClassNotFoundException ex) {
                            log.warn("Could not find class", ex);
                        }
                    }
                }
            } catch (ClassNotFoundException ex) {
                log.warn("Could not find class", ex);
            }

        }
        Document document = generator.getDocument();

        writeToFile(document, new File("applicationContext.xml"));
    }

    /**
     * The list of Java primitive types.
     */
    private static final List<String> primitives = Arrays
            .asList(new String[] { "byte", "short", "char", "int", "long", "float", "double", "boolean" });
    /**
     * The list of Java wrapper classes (includes String).
     */
    private static final List<String> wrappers = Arrays.asList(new String[] { "Byte", "Short", "Character",
            "Integer", "Long", "Float", "Double", "Boolean", "String" });
    /**
     * The XML document.
     */
    private final Document document;
    /**
     * The XML root element.
     */
    private final Element root;
    private LinkedHashMap<String, BeanDescriptor> classToElement = new LinkedHashMap<>();
    private HashMap<Class<?>, List<Object>> classToObject = new HashMap<>();
    private String defaultScope = "prototype";

    /**
     * Constructs a new application context generator.
     *
     * @param springVersion the Spring version
     * @param defaultScope
     */
    public ReflectionApplicationContextGenerator(String springVersion, String defaultScope) {
        //create the XML document
        DocumentBuilderFactory dbfac = DocumentBuilderFactory.newInstance();
        DocumentBuilder docBuilder = null;
        try {
            docBuilder = dbfac.newDocumentBuilder();
        } catch (ParserConfigurationException e) {
            //never thrown in my case, so ignore it
        }
        document = docBuilder.newDocument();

        //create the root element
        root = document.createElementNS("http://www.springframework.org/schema/beans", "beans");
        root.setAttributeNS("http://www.w3.org/2001/XMLSchema-instance", "schemaLocation",
                "http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-"
                        + springVersion + ".xsd");
        document.appendChild(root);
        this.defaultScope = defaultScope;
    }

    /**
     * Gets the generated XML document.
     *
     * @return the XML document
     */
    public Document getDocument() {
        return document;
    }

    /**
     * Adds a bean to the application context using a Java source file. Only
     * public classes are added.
     *
     * @param className
     * @return this
     * @throws java.lang.ClassNotFoundException
     */
    public ReflectionApplicationContextGenerator addBean(String className) throws ClassNotFoundException {
        Class<?> clazz;
        clazz = Class.forName(className);
        //        if (classToObject.containsKey(clazz)) {
        //            System.out.println("Class " + clazz.getCanonicalName() + " already known!");
        //            return this;
        //        }
        //        if (!classToObject.containsKey(clazz)) {
        createElement(clazz);
        //        }

        return this;
    }

    /**
     * Returns the known service providers for the given service interface name.
     *
     * @param serviceInterfaceName the fully qualified service interface class
     * name
     * @return the list of service providers
     */
    public List<?> getServiceProviders(String serviceInterfaceName) {
        try {
            return createServiceProviderElements(Class.forName(serviceInterfaceName));
        } catch (ClassNotFoundException ex) {
            log.warn("Exception while instantiating services:", ex);
            return Collections.emptyList();
        }
    }

    /**
     * Create a bean element for the given class.
     *
     * @param clazz the target class
     * @return the bean element or a list of beans for each service provider
     * available
     */
    public List<?> createElement(Class<?> clazz) {
        if (clazz.isInterface() || Modifier.isAbstract(clazz.getModifiers())) {
            return createServiceProviderElements(clazz);
        }
        if (clazz.getConstructors().length == 0) {
            log.warn("Class {} has no public no-argument constructor!", clazz);
            return Collections.emptyList();
        }
        Object obj;
        try {
            obj = clazz.newInstance();
            //build a "bean" element for each class
            buildBeanElement(obj);
            return Arrays.asList(obj);
        } catch (InstantiationException | IllegalAccessException ex) {
            Logger.getLogger(ReflectionApplicationContextGenerator.class.getName()).log(Level.SEVERE, null, ex);
        }
        return Collections.emptyList();
    }

    /**
     * Returns one prototype bean for each service provider known for the given
     * serviceInterface.
     *
     * @param serviceInterface the service interface to look for service
     * providers
     * @return the list of protype beans created by calling each service
     * provider
     */
    public List<?> createServiceProviderElements(Class<?> serviceInterface) {
        if (classToObject.containsKey(serviceInterface)) {
            return classToObject.get(serviceInterface);
        }
        LinkedList<Object> spis = new LinkedList<>(Lookup.getDefault().lookupAll(serviceInterface));
        for (Object obj : spis) {
            buildBeanElement(obj);
        }
        classToObject.put(serviceInterface, spis);
        return spis;
    }

    /**
     * Returns a version of <code>s</code>, with the first character in lower
     * case.
     *
     * @param s the string
     * @return s with lower-cased first character
     */
    public static String toLowerCaseName(String s) {
        return s.substring(0, 1).toLowerCase() + s.substring(1);
    }

    /**
     * Creates the <code>BeanDescriptor</code> element.
     *
     * @param obj the object to create a bean descriptor for
     * @return the bean descriptor
     */
    private BeanDescriptor buildBeanElement(Object obj) {
        String id = toLowerCaseName(obj.getClass().getSimpleName());
        if (classToElement.containsKey(id)) {
            System.out.println("BeanDescriptor with id " + id + " already present!");
        } else {
            BeanDescriptor bd = new BeanDescriptor(this, obj, defaultScope, id);
            Element beanElement = createElement(this, document, bd);
            if (beanElement != null) {
                Comment beanCommentElement = null;
                if (obj instanceof AFragmentCommand) {
                    AFragmentCommand afc = (AFragmentCommand) obj;
                    beanCommentElement = document.createComment(afc.getDescription());
                    root.appendChild(beanCommentElement);
                }
                root.appendChild(beanElement);
                System.out.println("Adding class " + obj.getClass().getCanonicalName());
                classToObject.put(obj.getClass(), Arrays.asList(obj));
                classToElement.put(id, bd);
            } else {
                //System.err.println("Warning: Could not find public class in \"" + file + "\".");
            }
        }
        return classToElement.get(id);
    }

    /**
     *
     * @param clazz
     * @param packages
     * @return
     */
    public static Set<BeanDefinition> getImplementationsOf(Class<?> clazz, String... packages) {
        ClassPathScanningCandidateComponentProvider provider = new ClassPathScanningCandidateComponentProvider(
                false);
        provider.addIncludeFilter(new AssignableTypeFilter(clazz));
        Set<BeanDefinition> beanDefinitions = new LinkedHashSet<>();
        for (String pkg : packages) {
            String pkgDeclaration = "";
            if (pkg.contains(".")) {
                pkgDeclaration = pkg.replaceAll("\\.", "/");
            }
            Set<BeanDefinition> components = provider.findCandidateComponents(pkgDeclaration);
            beanDefinitions.addAll(components);
        }
        return beanDefinitions;
    }

    /**
     *
     * @param field
     * @return
     */
    public static Class<?> getGenericFieldType(Field field) {
        Type fieldType = field.getGenericType();
        if (fieldType instanceof ParameterizedType) {
            Type[] t = ((ParameterizedType) fieldType).getActualTypeArguments();
            if (t != null) {
                return (Class<?>) t[0];
            }
        }
        return field.getType();
    }

    /**
     *
     * @param m
     * @return
     */
    public static Class<?> getGenericMethodReturnType(Method m) {
        Type methodReturnType = m.getGenericReturnType();
        if (methodReturnType instanceof ParameterizedType) {
            Type[] t = ((ParameterizedType) methodReturnType).getActualTypeArguments();
            if (t != null) {
                return (Class<?>) t[0];
            }
        }
        return m.getReturnType();
    }

    /**
     * Checks and adds type information for the given method, class and object
     * to the properties list.
     *
     * @param method the method to check for a property
     * @param javaClass the class to check for settable property
     * @param obj the stub / default object of type javaClass
     * @param properties the properties list
     */
    public static void checkMutableProperties(Method method, Class<?> javaClass, Object obj,
            List<ObjectProperty> properties) {
        if (method.getName().startsWith("get") || method.getName().startsWith("is")) {
            String methodBaseName = method.getName().startsWith("get") ? method.getName().substring(3)
                    : method.getName().substring(2);
            try {
                //check for corresponding setter
                if (javaClass.getMethod("set" + methodBaseName, method.getReturnType()) != null) {
                    String propertyName = toLowerCaseName(methodBaseName);
                    //                            System.out.print("Handling property " + propertyName);
                    ObjectProperty p = new ObjectProperty();
                    Class<?> returnType = method.getReturnType();
                    if (returnType.isArray()) {
                        p.type = "Array";
                    } else {
                        p.type = returnType.getCanonicalName();
                    }
                    Class<?> genericReturnType = getGenericMethodReturnType(method);
                    p.genericType = genericReturnType.getCanonicalName();
                    p.name = propertyName;

                    Object methodObject = null;
                    try {
                        methodObject = method.invoke(obj);
                    } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException ex) {
                        log.warn(ex.getLocalizedMessage());
                    }
                    String value = null;
                    if (methodObject == null) {
                        value = "";
                    } else {
                        if (methodObject.getClass().isArray()) {
                            value = methodObject.getClass().getComponentType().getCanonicalName();
                        } else {
                            value = methodObject.toString();
                        }
                    }
                    if (value == null) {
                        value = "";
                    } else {
                        value = value.trim();
                    }
                    if (value.contains("\"")) {
                        //remove the quotes that surround Strings
                        value = value.replaceAll("\"", "");
                    } else if (value.contains("'")) {
                        //remove the quotes that surround characters
                        value = value.replaceAll("'", "");
                    } else if (value.endsWith("d") || value.endsWith("D") || value.endsWith("f")
                            || value.endsWith("F") || value.endsWith("l") || value.endsWith("L")) {
                        //remove the "double", "float", or "long" letters if they are there
                        value = value.substring(0, value.length() - 1);
                    }
                    p.value = value;

                    System.out.println(" value: " + p.value + " type: " + p.type);
                    properties.add(p);
                }
            } catch (NoSuchMethodException ex) {
                log.info("Ignoring read-only property {}", methodBaseName);
                //Logger.getLogger(ReflectionApplicationContextGenerator.class.getName()).log(Level.SEVERE, null, ex);
            } catch (SecurityException ex) {
                log.warn(ex.getLocalizedMessage());
            }
        }
    }

    /**
     * Creates an xml element using the given document for element creation.
     *
     * @param generator
     * @param document the document use for element creation
     * @param beanDescriptor
     * @return the modified element
     */
    public static Element createElement(ReflectionApplicationContextGenerator generator, Document document,
            BeanDescriptor beanDescriptor) {
        Class<?> javaClass = beanDescriptor.clazz;
        System.out.println("Building bean element for " + javaClass.getName());
        //get the name of the class
        String className = javaClass.getSimpleName();

        //get the name of the package
        String packageName = javaClass.getPackage().getName();

        //create <bean /> element
        Element beanElement = document.createElement("bean");
        String classNameLower = toLowerCaseName(className);
        beanElement.setAttribute("id", classNameLower);
        String classAttr = (packageName == null) ? className : packageName + "." + className;
        beanElement.setAttribute("class", classAttr);
        beanElement.setAttribute("scope", beanDescriptor.scope);
        //constructors are not supported
        //get all the class' properties from the public fields and setter methods.

        for (Method method : javaClass.getMethods()) {
            checkMutableProperties(method, javaClass, beanDescriptor.obj, beanDescriptor.properties);
        }
        //sort by name
        Collections.sort(beanDescriptor.properties, new Comparator<ObjectProperty>() {
            @Override
            public int compare(ObjectProperty t, ObjectProperty t1) {
                return t.name.compareTo(t1.name);
            }
        });
        List<String> blackList = Arrays.asList("workflow", "progress", "cvResolver");
        //add all properties as <property /> elements
        for (ObjectProperty p : beanDescriptor.properties) {
            if (!blackList.contains(p.name)) {
                Element propertyElement = document.createElement("property");
                propertyElement.setAttribute("name", p.name);
                Comment propertyCommentElement = document
                        .createComment(AnnotationInspector.getDescriptionFor(javaClass, p.name));
                boolean append = true;
                if (p.type.startsWith("java.lang.")) {
                    String shortType = p.type.substring("java.lang.".length());
                    if (primitives.contains(shortType) || wrappers.contains(shortType)) {
                        propertyElement.setAttribute("value", p.value);
                    }
                } else if (primitives.contains(p.type) || wrappers.contains(p.type)) {
                    propertyElement.setAttribute("value", p.value);
                } else if ("Array".equals(p.type) || "List".equals(p.type) || "java.util.List".equals(p.type)) {
                    Element listElement = document.createElement("list");
                    String genericType = p.genericType;
                    propertyElement.appendChild(listElement);
                } else if ("Set".equals(p.type) || "java.util.Set".equals(p.type)) {
                    Element listElement = document.createElement("set");
                    propertyElement.appendChild(listElement);
                } else if ("Map".equals(p.type) || "java.util.Map".equals(p.type)) {
                    Element listElement = document.createElement("map");
                    propertyElement.appendChild(listElement);
                } else if ("Properties".equals(p.type) || "java.util.Properties".equals(p.type)) {
                    Element listElement = document.createElement("props");
                    propertyElement.appendChild(listElement);
                } else {
                    try {
                        //                    System.err.println("Skipping ref!");
                        Set<BeanDefinition> beanDefinitions = getImplementationsOf(Class.forName(p.type), "cross",
                                "maltcms", "net.sf.maltcms");
                        BeanDefinition first = null;
                        for (BeanDefinition bd : beanDefinitions) {
                            generator.addBean(bd.getBeanClassName());
                            if (first == null) {
                                first = bd;
                            }
                        }
                        if (first != null) {
                            String simpleName = first.getBeanClassName()
                                    .substring(first.getBeanClassName().lastIndexOf(".") + 1);
                            propertyElement.setAttribute("ref",
                                    generator.classToElement.get(toLowerCaseName(simpleName)).id);
                        }
                    } catch (ClassNotFoundException ex) {
                        Logger.getLogger(ReflectionApplicationContextGenerator.class.getName()).log(Level.SEVERE,
                                null, ex);
                    }
                    append = true;
                    //                    try {
                    //                        generator.addBean(p.type);
                    //                        Class<?> c = Class.forName(p.type);
                    //                        List<Object> objects = generator.classToObject.get(c);
                    //                        if (objects != null && !objects.isEmpty()) {
                    //                            propertyElement.setAttribute("ref", generator.buildBeanElement(objects.get(0)).id);
                    //                        } else {
                    //                            append = false;
                    //                        }
                    //                    } catch (ClassNotFoundException ex) {
                    //                        Logger.getLogger(ReflectionApplicationContextGenerator.class.getName()).log(Level.SEVERE, null, ex);
                    //                    }

                }
                if (append) {
                    beanElement.appendChild(propertyCommentElement);
                    beanElement.appendChild(propertyElement);
                } else {
                    beanElement.appendChild(propertyCommentElement);
                    Comment comment = document.createComment("<property name=\"" + p.name + "\" ref=\"\"/>");
                    beanElement.appendChild(comment);
                }
            }
        }
        return beanElement;
    }

    /**
     * Simple holder class for Object related properties.
     */
    public static class ObjectProperty {

        /**
         * The name of the object property
         */
        public String name;
        /**
         * The class type of the object property
         */
        public String type;
        /**
         * The value of the object property
         */
        public String value;

        /**
         * The generic class type of the object property
         */
        public String genericType;
    }

    /**
     * Holder class for bean information.
     */
    public static class BeanDescriptor {

        private final ReflectionApplicationContextGenerator generator;
        private final String scope;
        private final Class<?> clazz;
        private final Object obj;
        /**
         *
         */
        public String id;
        /**
         *
         */
        public List<ObjectProperty> properties = new ArrayList<>();

        /**
         * Creates a new bean descriptor from the given information.
         *
         * @param generator
         * @param obj
         * @param scope
         * @param id
         */
        public BeanDescriptor(ReflectionApplicationContextGenerator generator, Object obj, String scope,
                String id) {
            this.generator = generator;
            this.clazz = obj.getClass();
            this.obj = obj;
            this.scope = scope;
            this.id = id;
        }

    }
}