de.akra.idocit.java.services.ReflectionHelper.java Source code

Java tutorial

Introduction

Here is the source code for de.akra.idocit.java.services.ReflectionHelper.java

Source

/*******************************************************************************
 * Copyright 2011, 2012 AKRA GmbH
 *
 * 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 de.akra.idocit.java.services;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;
import java.util.logging.Level;
import java.util.logging.Logger;

import org.eclipse.jdt.core.IJavaElement;
import org.eclipse.jdt.core.IType;
import org.eclipse.jdt.core.JavaModelException;
import org.eclipse.jdt.core.dom.ASTNode;
import org.eclipse.jdt.core.dom.ArrayType;
import org.eclipse.jdt.core.dom.IMethodBinding;
import org.eclipse.jdt.core.dom.ITypeBinding;
import org.eclipse.jdt.core.dom.Modifier;
import org.eclipse.jdt.core.dom.ParameterizedType;
import org.eclipse.jdt.core.dom.PrimitiveType;
import org.eclipse.jdt.core.dom.QualifiedType;
import org.eclipse.jdt.core.dom.SimpleType;
import org.eclipse.jdt.core.dom.Type;
import org.eclipse.jdt.core.dom.WildcardType;

import de.akra.idocit.common.structure.Numerus;
import de.akra.idocit.common.structure.SignatureElement;
import de.akra.idocit.java.constants.Constants;
import de.akra.idocit.java.structure.JavaAttribute;
import de.akra.idocit.java.structure.JavaParameter;

/**
 * Contains helper-methods for Java Reflection purposes.
 * 
 * @author Dirk Meier-Eickhoff
 * @since 0.0.1
 * @version 0.0.2
 * 
 */
public class ReflectionHelper {
    /**
     * Logger
     */
    private static Logger logger = Logger.getLogger(ReflectionHelper.class.getName());

    /**
     * The identifier for super classes.
     */
    private static final String SUPER_CLASS_IDENTIFIER = "super";

    private static final String GETTER_PREFIX = "get";
    private static final String BOOLEAN_GETTER_PREFIX = "is";
    private static final String SETTER_PREFIX = "set";

    /**
     * Set of already reflected types. If a type is already in the set, stop reflection to
     * avoid infinite loops.
     */
    private Set<String> reflectedTypes;

    /**
     * Resets the set {@link #reflectedTypes}. Must be invoked before starting the type
     * reflection.
     * 
     * @object This instance
     * @source No attributes are set, but the set of reflected types is cleared.
     * 
     * @thematicgrid Resetting Operations
     */
    private void resetReflectedTypes() {
        this.reflectedTypes = new HashSet<String>();
    }

    /**
     * Creates a {@link JavaParameter} with attributes read from the given
     * <code>typeBinding</code>.
     * 
     * @param parent
     *            [DESTINATION] Parent of the newly created Java-Parameter
     * 
     * @param typeBinding
     *            [SOURCE] The {@link ITypeBinding} belonging to the parameter that should
     *            be analysed with Java Reflection.
     * 
     * @param identifier
     *            [ATTRIBUTE]
     * 
     * @param qualifiedIdentifier
     *            [ATTRIBUTE]
     * 
     * @return [OBJECT] Never <code>null</code>
     * @thematicgrid Creating Operations
     */
    public JavaParameter createParameter(final SignatureElement parent, final ITypeBinding typeBinding,
            final String identifier, final String qualifiedIdentifier) {
        resetReflectedTypes();
        return doReflectParameter(parent, typeBinding, identifier, qualifiedIdentifier);
    }

    /**
     * Rule: Returns <code>true</code> with the Java Type represented by the given binding
     * implements the interface "java.util.Collection".
     * 
     * @param type
     *            [OBJECT]
     * 
     * @return [REPORT]
     * @thematicgrid Checking Operations
     */
    private static boolean isCollection(final ITypeBinding type) {
        if (type != null) {
            final ITypeBinding[] implementedTypes = type.getInterfaces();

            if (implementedTypes != null) {
                for (final ITypeBinding implementedType : implementedTypes) {
                    final String qualifiedName = implementedType.getQualifiedName();
                    if ((qualifiedName != null) && qualifiedName.startsWith("java.util.Collection")) {
                        return true;
                    }
                }
            }
        }

        return false;
    }

    /**
     * Rule: Returns <code>true</code> if the Java-Class represented by the given binding
     * has member variables which are accessible via a getter and a setter. The getter and
     * setter must follow the naming conventions defined by the Java Beans Specification:<br/>
     * <br/>
     * http://www.oracle.com/technetwork/java/javase/documentation/spec-136004.html
     * 
     * @param type
     *            [OBJECT]
     * 
     * @return [REPORT]
     * @thematicgrid Checking Operations
     */
    public boolean hasPublicAccessableAttributes(final ITypeBinding type) {
        return (type != null) && !findAttributesWithPublicGetterOrSetter(type.getDeclaredMethods()).isEmpty();
    }

    /**
     * @rule Rule: Returns {@link Numerus.PLURAL} if the given binding represents a Java
     *       Collection.
     * 
     * @param type
     *            [ATTRIBUTE]
     * 
     * @return [OBJECT]
     * @thematicgrid Concluding Operations
     */
    public Numerus deriveNumerus(ITypeBinding type) {
        return isCollection(type) ? Numerus.PLURAL : Numerus.SINGULAR;
    }

    /**
     * @see #createParameter(SignatureElement, ITypeBinding, String, String)
     */
    private JavaParameter doReflectParameter(final SignatureElement parent, final ITypeBinding typeBinding,
            final String identifier, final String qualifiedIdentifier) {
        final JavaParameter newParameter = new JavaParameter(parent, deriveNumerus(typeBinding),
                hasPublicAccessableAttributes(typeBinding));
        newParameter.setIdentifier(identifier);
        newParameter.setQualifiedIdentifier(qualifiedIdentifier);
        newParameter.setDataTypeName(typeBinding.getName());

        String qTypeName = typeBinding.getQualifiedName();
        qTypeName = qTypeName != null ? qTypeName : typeBinding.getName();
        newParameter.setQualifiedDataTypeName(qTypeName);

        if (!reflectedTypes.contains(qTypeName)) {
            reflectedTypes.add(qTypeName);
            final List<JavaAttribute> accessableAttributes = findAttributesWithPublicGetterOrSetter(
                    typeBinding.getDeclaredMethods());

            for (final JavaAttribute attribute : accessableAttributes) {
                newParameter.addParameter(doReflectParameter(newParameter, attribute.getType(), attribute.getName(),
                        qTypeName + JavaParser.delimiters.getNamespaceDelimiter() + attribute.getName()));
            }

            // check super classes for attributes with public getter or setter
            final ITypeBinding superType = typeBinding.getSuperclass();

            if (superType != null && !superType.getQualifiedName().equals(Object.class.getName())) {
                newParameter.addParameter(doReflectParameter(newParameter, superType, SUPER_CLASS_IDENTIFIER,
                        SUPER_CLASS_IDENTIFIER));
            }

            // remove type again, because reflecting this type ends
            reflectedTypes.remove(qTypeName);
        } else {
            logger.fine("Recursion terminated for type \"" + qTypeName + "\"");
        }

        return newParameter;
    }

    /**
     * Creates a {@link JavaParameter} with attributes read from the given
     * <code>typeBinding</code>.
     * 
     * @param parent
     *            [DESTINATION] Parent of the newly created Java-Parameter
     * 
     * @param type
     *            [SOURCE] The {@link ITypeBinding} belonging to the parameter that should
     *            be analysed with Java Reflection.
     * 
     * @param identifier
     *            [ATTRIBUTE]
     * 
     * @param qualifiedIdentifier
     *            [ATTRIBUTE]
     * 
     * @return [OBJECT] Never <code>null</code>
     * @thematicgrid Creating Operations
     */
    public JavaParameter createParameter(final SignatureElement parent, final Type type, final String identifier,
            final String qualifiedIdentifier) {
        resetReflectedTypes();
        return doReflectParameter(parent, type, identifier, qualifiedIdentifier);
    }

    /**
     * @see #createParameter(SignatureElement, Type, String, String)
     */
    private JavaParameter doReflectParameter(final SignatureElement parent, final Type type,
            final String identifier, final String qualifiedIdentifier) {
        // try to resolve binding on Type
        final ITypeBinding typeBinding = type.resolveBinding();
        if (typeBinding != null) {
            return doReflectParameter(parent, typeBinding, identifier, qualifiedIdentifier);
        } else {
            // We know that the typebinding is null, so the numerus must be
            // singular and there are no public accessable attributes!
            final JavaParameter returnParameter = new JavaParameter(parent, Numerus.SINGULAR, false);
            returnParameter.setIdentifier(identifier);
            returnParameter.setQualifiedIdentifier(qualifiedIdentifier);
            final String typeName = extractIdentifierFrom(type);
            returnParameter.setDataTypeName(typeName);
            returnParameter.setQualifiedDataTypeName(typeName);
            return returnParameter;
        }
    }

    /**
     * Searches for all accessible attributes according to the Java Beans Standard. There
     * must be a getter with the method name prefix "get" or "is" and a setter with the
     * method name prefix "set" for an attribute.
     * 
     * @param methodBindings
     *            [SOURCE] The methods declared in the class.
     * 
     * @return [OBJECT] List with attributes ({@link JavaAttribute}) that have a public
     *         getter and setter.
     * @since 0.0.2
     * @thematicgrid Searching Operations
     */
    private static List<JavaAttribute> findAttributesWithPublicGetterOrSetter(
            final IMethodBinding[] methodBindings) {
        final Set<JavaAttribute> foundGetterAndSetter = new TreeSet<JavaAttribute>();
        final Set<JavaAttribute> foundGetter = new TreeSet<JavaAttribute>();
        final Set<JavaAttribute> foundSetter = new TreeSet<JavaAttribute>();

        for (final IMethodBinding methodBinding : methodBindings) {
            if (isPublic(methodBinding.getModifiers()) || isInInterface(methodBinding)) {
                final ITypeBinding[] parameterTypes = methodBinding.getParameterTypes();
                final String methodName = methodBinding.getName();

                if (methodName.startsWith(GETTER_PREFIX) || methodName.startsWith(BOOLEAN_GETTER_PREFIX)) {
                    // consider only getter with a return type and 0 parameters
                    if (!Constants.RETURN_TYPE_VOID.equals(methodBinding.getReturnType().getName())
                            && (parameterTypes == null || parameterTypes.length == 0)) {
                        final String prefix = methodName.startsWith(GETTER_PREFIX) ? GETTER_PREFIX
                                : BOOLEAN_GETTER_PREFIX;
                        final JavaAttribute javaAttribute = new JavaAttribute(
                                extractAttibuteName(methodName, prefix), methodBinding.getReturnType());
                        if (foundSetter.contains(javaAttribute)) {
                            foundSetter.remove(javaAttribute);
                            foundGetterAndSetter.add(javaAttribute);
                        } else {
                            foundGetter.add(javaAttribute);
                        }
                    }
                } else if (methodName.startsWith(SETTER_PREFIX)) {
                    // consider only setter with one parameter
                    if (parameterTypes != null && parameterTypes.length == 1) {
                        final JavaAttribute javaAttribute = new JavaAttribute(
                                extractAttibuteName(methodName, SETTER_PREFIX), parameterTypes[0]);
                        if (foundGetter.contains(javaAttribute)) {
                            foundGetter.remove(javaAttribute);
                            foundGetterAndSetter.add(javaAttribute);
                        } else {
                            foundSetter.add(javaAttribute);
                        }
                    } else {
                        logger.log(Level.WARNING,
                                "Setter \"{0}\" has {1} parameters. Allowed is only 1 parameter for a setter.",
                                new Object[] { methodName, (parameterTypes != null ? parameterTypes.length : 0) });
                    }
                }
            }
        }

        return new ArrayList<JavaAttribute>(foundGetterAndSetter);
    }

    /**
     * Check if the method is declared in a Java interface.
     * 
     * @param methodBinding
     *            [OBJECT]
     * @return [REPORT] {@code true} if the method is in a Java interface.
     */
    private static boolean isInInterface(final IMethodBinding methodBinding) {
        boolean isInterface = false;
        final IJavaElement method = methodBinding.getJavaElement();
        if (method != null) {
            final IJavaElement parent = method.getParent();

            if (parent.getElementType() == IJavaElement.TYPE) {
                final IType type = (IType) parent;
                try {
                    isInterface = type.isInterface() && !type.isAnnotation();
                } catch (JavaModelException e) {
                    logger.log(Level.SEVERE, "Failed to check if the method is in an interface.", e);
                }
            }
        } else {
            logger.log(Level.INFO, "Method \"" + methodBinding.getName() + "\" has no parent element.");
        }
        return isInterface;
    }

    /**
     * Convert the {@code methodName} to the corresponding attribute name. The
     * {@code prefix} is cut and the first character followed by the prefix is swapped to
     * lower case.
     * 
     * @param methodName
     *            [SOURCE]
     * @param prefix
     *            [OBJECT] is removed from method name.
     * @return [DESTINATION] the extracted attribute name.
     */
    private static String extractAttibuteName(final String methodName, final String prefix) {
        return methodName.substring(prefix.length(), prefix.length() + 1).toLowerCase()
                + methodName.substring(prefix.length() + 1);
    }

    /**
     * Returns the identifier from the {@link Type} depending on the containing type.
     * 
     * @param type
     *            [SOURCE]
     * 
     * @return [OBJECT]
     * @see Type
     * @thematicgrid Extracting Operations
     */
    public static String extractIdentifierFrom(Type type) {
        switch (type.getNodeType()) {
        case ASTNode.PRIMITIVE_TYPE: {
            PrimitiveType primitiveType = (PrimitiveType) type;
            return primitiveType.getPrimitiveTypeCode().toString();
        }
        case ASTNode.ARRAY_TYPE: {
            ArrayType arrayType = (ArrayType) type;
            String name = extractIdentifierFrom(arrayType.getElementType());
            StringBuffer identifier = new StringBuffer(name);

            for (int i = 0; i < arrayType.getDimensions(); i++) {
                identifier.append("[]");
            }
            return identifier.toString();
        }
        case ASTNode.SIMPLE_TYPE: {
            SimpleType simpleType = (SimpleType) type;
            return simpleType.getName().getFullyQualifiedName();
        }
        case ASTNode.QUALIFIED_TYPE: {
            QualifiedType qType = (QualifiedType) type;
            return extractIdentifierFrom(qType.getQualifier()) + "." + qType.getName().getIdentifier();
        }
        case ASTNode.PARAMETERIZED_TYPE: {
            ParameterizedType paramType = (ParameterizedType) type;
            StringBuffer identifier = new StringBuffer(extractIdentifierFrom(paramType.getType()));
            identifier.append('<');

            @SuppressWarnings("unchecked")
            Iterator<Type> iterTypeArgs = (Iterator<Type>) paramType.typeArguments().iterator();
            while (iterTypeArgs.hasNext()) {
                Type typeArg = iterTypeArgs.next();
                identifier.append(extractIdentifierFrom(typeArg));
                if (iterTypeArgs.hasNext()) {
                    identifier.append(',');
                }
            }

            identifier.append('>');
            return identifier.toString();
        }
        case ASTNode.WILDCARD_TYPE: {
            WildcardType wildcard = (WildcardType) type;
            String identifier = "? extends ";
            if (!wildcard.isUpperBound()) {
                identifier = "? super ";
            }
            return identifier + extractIdentifierFrom(wildcard.getBound());
        }
        }
        return SignatureElement.ANONYMOUS_IDENTIFIER;
    }

    /**
     * Checks if the modifier {@link Modifier#PUBLIC} is in <code>modifiers</code>.
     * 
     * @param modifiers
     *            [OBJECT]
     * 
     * @return [REPORT]
     * @see Modifier
     * @thematicgrid Checking Operations
     */
    public static boolean isPublic(int modifiers) {
        return (modifiers & Modifier.PUBLIC) == Modifier.PUBLIC;
    }
}