ClassInspectionService.java

/*
 * Copyright (C) 2017 Red Hat, Inc.
 *
 * 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 io.atlasmap.java.inspect;

import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;
import java.lang.reflect.WildcardType;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import io.atlasmap.core.AtlasPath;
import io.atlasmap.core.AtlasUtil;
import io.atlasmap.java.core.ClassHelper;
import io.atlasmap.java.core.JdkPackages;
import io.atlasmap.java.core.StringUtil;
import io.atlasmap.java.v2.AtlasJavaModelFactory;
import io.atlasmap.java.v2.JavaClass;
import io.atlasmap.java.v2.JavaEnumField;
import io.atlasmap.java.v2.JavaField;
import io.atlasmap.java.v2.ModifierList;
import io.atlasmap.spi.AtlasConversionService;
import io.atlasmap.v2.CollectionType;
import io.atlasmap.v2.FieldStatus;
import io.atlasmap.v2.FieldType;
import io.atlasmap.v2.StringList;

public class ClassInspectionService {

    public static final int MAX_REENTRY_LIMIT = 1;
    public static final int MAX_ARRAY_DIM_LIMIT = 256; // JVM specification

    private static final Logger LOG = LoggerFactory.getLogger(ClassInspectionService.class);
    // limit

    private List<String> listClasses = new ArrayList<>(
            Arrays.asList("java.util.List", "java.util.ArrayList", "java.util.LinkedList", "java.util.Vector",
                    "java.util.Stack", "java.util.AbstractList", "java.util.AbstractSequentialList"));
    private List<String> mapClasses = new ArrayList<>(Arrays.asList("java.util.Map", "java.util.HashMap",
            "java.util.TreeMap", "java.util.Hashtable", "java.util.IdentityHashMap", "java.util.LinkedHashMap",
            "java.util.LinkedHashMap", "java.util.SortedMap", "java.util.WeakHashMap", "java.util.Properties",
            "java.util.concurrent.ConcurrentHashMap", "java.util.concurrent.ConcurrentMap"));
    private AtlasConversionService atlasConversionService = null;
    private List<String> fieldExclusions = new ArrayList<>(Arrays.asList("serialVersionUID"));
    private List<String> classNameExclusions = new ArrayList<>();
    private Boolean disablePackagePrivateOnlyFields = false;
    private Boolean disableProtectedOnlyFields = false;
    private Boolean disablePrivateOnlyFields = false;
    private Boolean disablePublicOnlyFields = false;
    private Boolean disablePublicGetterSetterFields = false;

    public List<String> getMapClasses() {
        return this.mapClasses;
    }

    public List<String> getListClasses() {
        return this.listClasses;
    }

    public List<String> getClassNameExclusions() {
        return this.classNameExclusions;
    }

    public List<String> getFieldExclusions() {
        return this.fieldExclusions;
    }

    public Boolean getDisableProtectedOnlyFields() {
        return disableProtectedOnlyFields;
    }

    public void setDisableProtectedOnlyFields(Boolean disableProtectedOnlyFields) {
        this.disableProtectedOnlyFields = disableProtectedOnlyFields;
    }

    public Boolean getDisablePackagePrivateOnlyFields() {
        return disablePackagePrivateOnlyFields;
    }

    public void setDisablePackagePrivateOnlyFields(Boolean disablePackagePrivateOnlyFields) {
        this.disablePackagePrivateOnlyFields = disablePackagePrivateOnlyFields;
    }

    public Boolean getDisablePrivateOnlyFields() {
        return disablePrivateOnlyFields;
    }

    public void setDisablePrivateOnlyFields(Boolean disablePrivateOnlyFields) {
        this.disablePrivateOnlyFields = disablePrivateOnlyFields;
    }

    public Boolean getDisablePublicOnlyFields() {
        return disablePublicOnlyFields;
    }

    public void setDisablePublicOnlyFields(Boolean disablePublicOnlyFields) {
        this.disablePublicOnlyFields = disablePublicOnlyFields;
    }

    public Boolean getDisablePublicGetterSetterFields() {
        return disablePublicGetterSetterFields;
    }

    public void setDisablePublicGetterSetterFields(Boolean disablePublicGetterSetterFields) {
        this.disablePublicGetterSetterFields = disablePublicGetterSetterFields;
    }

    public JavaClass inspectClass(String className, CollectionType collectionType, String collectionClassName) throws ClassNotFoundException {
        // Use a loader for this class for now
        ClassLoader classLoader = getClass().getClassLoader();
        return inspectClass(classLoader, className, collectionType, collectionClassName);
    }

    public JavaClass inspectClass(ClassLoader classLoader, String className, CollectionType collectionType, String collectionClassName)
        throws ClassNotFoundException {
        Class<?> clazz = classLoader.loadClass(className);
        return inspectClass(classLoader, clazz, collectionType, collectionClassName);
    }

    public JavaClass inspectClass(String className, CollectionType collectionType, String collectionClassName, String classpath) throws InspectionException {
        if (LOG.isDebugEnabled()) {
            LOG.debug("Inspecting class: " + className + ", classPath: " + classpath);
        }
        if (className == null || classpath == null) {
            throw new InspectionException("ClassName and Classpath must be specified");
        }

        JavaClass d = null;
        try {
            JarClassLoader jcl = new JarClassLoader(new String[] { "target/reference-jars" });
            Class<?> clazz = jcl.loadClass(className);
            d = inspectClass(jcl, clazz, collectionType, collectionClassName);
        } catch (ClassNotFoundException cnfe) {
            if (LOG.isDebugEnabled()) {
                LOG.debug("Class was not found: " + className);
            }
            d = AtlasJavaModelFactory.createJavaClass();
            d.setClassName(className);
            d.setStatus(FieldStatus.NOT_FOUND);
        }
        return d;
    }

    public JavaClass inspectClass(Class<?> clazz, CollectionType collectionType, String collectionClassName) {
        if (clazz == null) {
            throw new IllegalArgumentException("Class must be specified");
        }

        return inspectClass(clazz.getClassLoader(), clazz, collectionType, collectionClassName);
    }

    public JavaClass inspectClass(ClassLoader classLoader, Class<?> clazz, CollectionType collectionType, String collectionClassName) {
        if (clazz == null) {
            throw new IllegalArgumentException("Class must be specified");
        }

        JavaClass javaClass = AtlasJavaModelFactory.createJavaClass();
        javaClass.setCollectionType(collectionType);
        String rootPath = AtlasPath.PATH_SEPARATOR;
        if (collectionType == CollectionType.LIST) {
            rootPath += AtlasPath.PATH_LIST_START + AtlasPath.PATH_LIST_END;
        } else if (collectionType == CollectionType.ARRAY) {
            rootPath += AtlasPath.PATH_ARRAY_START + AtlasPath.PATH_ARRAY_END;
        } else if (collectionType == CollectionType.MAP) {
            rootPath += AtlasPath.PATH_MAP_START + AtlasPath.PATH_MAP_END;
        }
        if (collectionClassName != null && !collectionClassName.isEmpty()) {
            javaClass.setCollectionClassName(collectionClassName);
        }
        javaClass.setPath(rootPath);
        Set<String> cachedClasses = new HashSet<>();
        cachedClasses.add(clazz.getName()); // we cache ourself
        inspectClass(classLoader, clazz, javaClass, cachedClasses, rootPath);
        javaClass.setFieldType(getConversionService().fieldTypeFromClass(javaClass.getClassName()));
        return javaClass;
    }

    private void inspectClass(ClassLoader classLoader, Class<?> clazz, JavaClass javaClass, Set<String> cachedClasses,
            String pathPrefix) {

        Class<?> clz = clazz;
        if (clazz.isArray()) {
            javaClass.setArrayDimensions(detectArrayDimensions(clazz));
            javaClass.setCollectionType(CollectionType.ARRAY);
            clz = detectArrayClass(clazz);
            if (!javaClass.getPath().endsWith(AtlasPath.PATH_ARRAY_END)) {
                javaClass.setPath(javaClass.getPath() + AtlasPath.PATH_ARRAY_START + AtlasPath.PATH_ARRAY_END);
            }
        } else {
            clz = clazz;
        }

        if (isFieldMap(clz.getName())) {
            javaClass.setCollectionType(CollectionType.MAP);
        }

        javaClass.setClassName(clz.getName());
        javaClass.setCanonicalClassName(clz.getCanonicalName());
        javaClass.setPackageName((clz.getPackage() != null ? clz.getPackage().getName() : null));
        javaClass.setAnnotation(clz.isAnnotation());
        javaClass.setAnnonymous(clz.isAnonymousClass());
        javaClass.setEnumeration(clz.isEnum());
        javaClass.setInterface(clz.isInterface());
        javaClass.setLocalClass(clz.isLocalClass());
        javaClass.setMemberClass(clz.isMemberClass());
        javaClass.setPrimitive(clz.isPrimitive());
        javaClass.setSynthetic(clz.isSynthetic());

        if (javaClass.getUri() == null) {
            javaClass.setUri(String.format(AtlasJavaModelFactory.URI_FORMAT, AtlasUtil.escapeForUri(clz.getName())));
        }

        if (clz.isPrimitive() || JdkPackages.contains(clz.getPackage().getName())) {
            if (LOG.isTraceEnabled()) {
                LOG.trace("Skipping class " + clz.getName() + " which is a Jdk core class");
            }
            return;
        }

        // Process super class fields and methods first, so child class fields
        // and methods override
        Class<?> tmpClazz = clz;
        Class<?> superClazz = tmpClazz.getSuperclass();
        while (superClazz != null) {
            if (JdkPackages.contains(superClazz.getPackage().getName())) {
                if (LOG.isTraceEnabled()) {
                    LOG.trace("Skipping SuperClass " + superClazz.getName() + " which is a Jdk core class");
                }
                superClazz = null;
            } else {
                inspectClassFields(classLoader, superClazz, javaClass, cachedClasses, pathPrefix);
                inspectClassMethods(classLoader, superClazz, javaClass, cachedClasses, pathPrefix);
                tmpClazz = superClazz;
                superClazz = tmpClazz.getSuperclass();
            }
        }

        inspectClassFields(classLoader, clz, javaClass, cachedClasses, pathPrefix);

        Object[] enumConstants = clz.getEnumConstants();
        if (enumConstants != null) {
            javaClass.setEnumeration(true);
            for (Object o : enumConstants) {
                JavaEnumField out = new JavaEnumField();
                if (o instanceof Enum) {
                    Enum<?> in = (Enum<?>) o;
                    out.setName(in.name());
                    out.setOrdinal(in.ordinal());
                    javaClass.getJavaEnumFields().getJavaEnumField().add(out);
                    out.setStatus(FieldStatus.SUPPORTED);
                } else {
                    out.setClassName(o.getClass().getName());
                    out.setStatus(FieldStatus.ERROR);
                }
            }
        } else {
            javaClass.setEnumeration(false);
        }

        inspectClassMethods(classLoader, clz, javaClass, cachedClasses, pathPrefix);

        if (javaClass.getModifiers() == null) {
            javaClass.setModifiers(new ModifierList());
        } else {
            javaClass.getModifiers().getModifier().clear();
        }
        javaClass.getModifiers().getModifier().addAll(detectModifiers(clz.getModifiers()));

        // TODO: annotations, generics, enums, class modifiers (public,
        // synchronized, etc),
        // more of these here:
        // https://docs.oracle.com/javase/8/docs/api/java/lang/Class.html#isPrimitive--
        // TODO: exceptions
        // TODO: lists
        // return javaClass;
    }

    private JavaField inspectGetMethod(ClassLoader classLoader, Method m, JavaField s, Set<String> cachedClasses,
            String pathPrefix) {
        JavaField field = s;

        field.setName(StringUtil.getFieldNameFromGetter(m.getName()));

        if (pathPrefix != null && pathPrefix.length() > 0) {
            field.setPath(pathPrefix
                    + (pathPrefix.endsWith(AtlasPath.PATH_SEPARATOR) ? "" : AtlasPath.PATH_SEPARATOR)
                    + StringUtil.getFieldNameFromGetter(m.getName()));
        } else {
            field.setPath(StringUtil.getFieldNameFromGetter(m.getName()));
        }

        if (m.getParameterCount() != 0) {
            field.setStatus(FieldStatus.UNSUPPORTED);
            return field;
        }

        if (m.getReturnType().equals(Void.TYPE)) {
            field.setStatus(FieldStatus.UNSUPPORTED);
            return field;
        }

        Class<?> returnType = m.getReturnType();
        if (returnType.isArray()) {
            field.setCollectionType(CollectionType.ARRAY);
            field.setArrayDimensions(detectArrayDimensions(returnType));
            field.setPath(field.getPath() + AtlasPath.PATH_ARRAY_START + AtlasPath.PATH_ARRAY_END);
            returnType = detectArrayClass(returnType);
        } else if (Collection.class.isAssignableFrom(returnType)) {
            field.setCollectionType(CollectionType.LIST);
            field.setCollectionClassName(returnType.getName());
            field.setPath(field.getPath() + AtlasPath.PATH_LIST_START + AtlasPath.PATH_LIST_END);
            returnType = detectListClassFromMethodReturn(m);
        }

        field.setClassName(returnType.getName());
        field.setCanonicalClassName(returnType.getCanonicalName());
        field.setGetMethod(m.getName());
        field.setFieldType(getConversionService().fieldTypeFromClass(returnType));
        if (getConversionService().isPrimitive(returnType) || getConversionService().isBoxedPrimitive(returnType)) {
            field.setPrimitive(true);
            field.setStatus(FieldStatus.SUPPORTED);
        } else if (field.getFieldType() != FieldType.COMPLEX) {
            field.setPrimitive(false);
            field.setStatus(FieldStatus.SUPPORTED);
        } else {
            field.setPrimitive(false);

            Class<?> complexClazz = null;
            JavaClass tmpField = convertJavaFieldToJavaClass(field);
            field = tmpField;

            if (returnType.getName() == null) {
                field.setStatus(FieldStatus.UNSUPPORTED);
            } else if (!cachedClasses.contains(returnType.getName())) {
                try {
                    complexClazz = classLoader.loadClass(returnType.getName());
                    cachedClasses.add(returnType.getName());
                    inspectClass(classLoader, complexClazz, tmpField, cachedClasses, field.getPath());
                    if (tmpField.getStatus() == null) {
                        field.setStatus(FieldStatus.SUPPORTED);
                    }
                } catch (ClassNotFoundException cnfe) {
                    field.setStatus(FieldStatus.NOT_FOUND);
                }
            } else {
                field.setStatus(FieldStatus.CACHED);
            }
        }

        return field;
    }

    private JavaField inspectSetMethod(ClassLoader classLoader, Method m, JavaField s, Set<String> cachedClasses,
            String pathPrefix) {
        JavaField field = s;

        field.setName(StringUtil.getFieldNameFromSetter(m.getName()));

        if (pathPrefix != null && pathPrefix.length() > 0) {
            field.setPath(pathPrefix
                    + (pathPrefix.endsWith(AtlasPath.PATH_SEPARATOR) ? "" : AtlasPath.PATH_SEPARATOR)
                    + StringUtil.getFieldNameFromSetter(m.getName()));
        } else {
            field.setPath(StringUtil.getFieldNameFromSetter(m.getName()));
        }

        if (m.getParameterCount() != 1) {
            field.setStatus(FieldStatus.UNSUPPORTED);
            return field;
        }

        if (!m.getReturnType().equals(Void.TYPE)) {
            field.setStatus(FieldStatus.UNSUPPORTED);
            return field;
        }

        Class<?>[] params = m.getParameterTypes();
        if (params == null || params.length != 1) {
            field.setStatus(FieldStatus.UNSUPPORTED);
            return field;
        }

        Class<?> paramType = params[0];
        if (paramType.isArray()) {
            field.setCollectionType(CollectionType.ARRAY);
            field.setArrayDimensions(detectArrayDimensions(paramType));
            field.setPath(field.getPath() + AtlasPath.PATH_ARRAY_START + AtlasPath.PATH_ARRAY_END);
            paramType = detectArrayClass(paramType);
        } else if (Collection.class.isAssignableFrom(paramType)) {
            field.setCollectionType(CollectionType.LIST);
            field.setCollectionClassName(paramType.getName());
            field.setPath(field.getPath() + AtlasPath.PATH_LIST_START + AtlasPath.PATH_LIST_END);
            paramType = detectListClassFromMethodParameter(m);
        }

        field.setClassName(paramType.getName());
        field.setCanonicalClassName(paramType.getCanonicalName());
        field.setSetMethod(m.getName());
        field.setFieldType(getConversionService().fieldTypeFromClass(paramType));
        if (getConversionService().isPrimitive(paramType) || getConversionService().isBoxedPrimitive(paramType)) {
            field.setPrimitive(true);
            field.setStatus(FieldStatus.SUPPORTED);
        } else if (field.getFieldType() != FieldType.COMPLEX) {
            field.setPrimitive(false);
            field.setStatus(FieldStatus.SUPPORTED);
        } else {
            field.setPrimitive(false);

            Class<?> complexClazz = null;
            JavaClass tmpField = convertJavaFieldToJavaClass(field);
            field = tmpField;

            if (paramType.getName() == null) {
                field.setStatus(FieldStatus.UNSUPPORTED);
            } else if (!cachedClasses.contains(paramType.getName())) {
                try {
                    complexClazz = classLoader.loadClass(paramType.getName());
                    cachedClasses.add(paramType.getName());
                    inspectClass(classLoader, complexClazz, tmpField, cachedClasses, field.getPath());
                    if (tmpField.getStatus() == null) {
                        field.setStatus(FieldStatus.SUPPORTED);
                    }
                } catch (ClassNotFoundException cnfe) {
                    field.setStatus(FieldStatus.NOT_FOUND);
                }
            } else {
                field.setStatus(FieldStatus.CACHED);
            }
        }

        return field;
    }

    private JavaField inspectField(ClassLoader classLoader, Field f, Set<String> cachedClasses, String pathPrefix) {

        JavaField s = AtlasJavaModelFactory.createJavaField();
        Class<?> clazz = f.getType();
        s.setName(f.getName());

        if (pathPrefix != null && pathPrefix.length() > 0) {
            s.setPath(pathPrefix
                    + (pathPrefix.endsWith(AtlasPath.PATH_SEPARATOR) ? "" : AtlasPath.PATH_SEPARATOR)
                    + f.getName());
        } else {
            s.setPath(f.getName());
        }

        if (isFieldMap(clazz.getName())) {
            s.setCollectionType(CollectionType.MAP);
            s.setPath(s.getPath() + AtlasPath.PATH_MAP_START + AtlasPath.PATH_MAP_END);
        }

        if (clazz.isArray()) {
            s.setCollectionType(CollectionType.ARRAY);
            s.setArrayDimensions(detectArrayDimensions(clazz));
            s.setPath(s.getPath() + AtlasPath.PATH_ARRAY_START + AtlasPath.PATH_ARRAY_END);
            clazz = detectArrayClass(clazz);
        } else if (Collection.class.isAssignableFrom(clazz)) {
            s.setCollectionType(CollectionType.LIST);
            s.setCollectionClassName(clazz.getName());
            s.setPath(s.getPath() + AtlasPath.PATH_LIST_START + AtlasPath.PATH_LIST_END);
            try {
                clazz = detectListClass(classLoader, f);
                if (clazz == null) {
                    s.setStatus(FieldStatus.ERROR);
                    return s;
                }
            } catch (ClassCastException | ClassNotFoundException cce) {
                LOG.debug("Error detecting inner listClass: " + cce.getMessage() + " for field: " + f.getName(), cce);
                s.setStatus(FieldStatus.ERROR);
                return s;
            }
        }

        s.setFieldType(getConversionService().fieldTypeFromClass(clazz));
        if (getConversionService().isPrimitive(clazz) || getConversionService().isBoxedPrimitive(clazz)) {
            s.setPrimitive(true);
            s.setStatus(FieldStatus.SUPPORTED);
        } else if (s.getFieldType() != FieldType.COMPLEX) {
            s.setPrimitive(false);
            s.setStatus(FieldStatus.SUPPORTED);
        } else {
            s.setPrimitive(false);

            Class<?> complexClazz = null;
            JavaClass tmpField = convertJavaFieldToJavaClass(s);
            s = tmpField;

            if (clazz.getName() == null) {
                s.setStatus(FieldStatus.UNSUPPORTED);
            } else if (!cachedClasses.contains(clazz.getName())) {
                try {
                    complexClazz = classLoader.loadClass(clazz.getName());
                    cachedClasses.add(clazz.getName());
                    inspectClass(classLoader, complexClazz, tmpField, cachedClasses, s.getPath());
                    if (tmpField.getStatus() == null) {
                        s.setStatus(FieldStatus.SUPPORTED);
                    }
                } catch (ClassNotFoundException cnfe) {
                    s.setStatus(FieldStatus.NOT_FOUND);
                }
            } else {
                s.setStatus(FieldStatus.CACHED);
            }
        }

        s.setClassName(clazz.getName());
        s.setCanonicalClassName(clazz.getCanonicalName());
        s.setSynthetic(f.isSynthetic());

        Annotation[] annotations = f.getAnnotations();
        if (annotations != null) {
            for (Annotation a : annotations) {
                if (s.getAnnotations() == null) {
                    s.setAnnotations(new StringList());
                }
                s.getAnnotations().getString().add(a.annotationType().getName());
            }
        }

        if (s.getModifiers() == null) {
            s.setModifiers(new ModifierList());
        }
        s.getModifiers().getModifier().addAll(detectModifiers(f.getModifiers()));

        List<String> pTypes = detectParameterizedTypes(f, false);
        if (pTypes != null) {
            if (s.getParameterizedTypes() == null) {
                s.setParameterizedTypes(new StringList());
            }
            s.getParameterizedTypes().getString().addAll(pTypes);
        }

        populateGetterSetter(clazz, f, s);
        return s;
    }

    private void populateGetterSetter(Class<?> clazz, Field reflectionField, JavaField atlasField) {
        try {
            String getterName = "get" + StringUtil.capitalizeFirstLetter(reflectionField.getName());
            reflectionField.getDeclaringClass().getMethod(getterName);
            atlasField.setGetMethod(getterName);
        } catch (NoSuchMethodException e) {
            if (LOG.isDebugEnabled()) {
                LOG.debug("No 'get' method for field named: " + reflectionField.getName() + " in class: "
                        + reflectionField.getDeclaringClass().getName());
            }
        }
        if (atlasField.getGetMethod() == null && ("boolean".equals(atlasField.getClassName())
                || "java.lang.Boolean".equals(atlasField.getClassName()))) {
            try {
                String getterName = "is" + StringUtil.capitalizeFirstLetter(reflectionField.getName());
                reflectionField.getDeclaringClass().getMethod(getterName);
                atlasField.setGetMethod(getterName);
            } catch (NoSuchMethodException e) {
                if (LOG.isDebugEnabled()) {
                    LOG.debug("No 'is' method for field named: " + reflectionField.getName() + " in class: "
                            + reflectionField.getDeclaringClass().getName());
                }
            }
        }
        try {
            String setterName = "set" + StringUtil.capitalizeFirstLetter(reflectionField.getName());
            reflectionField.getDeclaringClass().getMethod(setterName, clazz);
            atlasField.setSetMethod(setterName);
        } catch (NoSuchMethodException e) {
            if (LOG.isDebugEnabled()) {
                LOG.debug("No 'set' method for field named: " + reflectionField.getName() + " in class: "
                        + reflectionField.getDeclaringClass().getName());
            }
        }
    }

    private void inspectClassFields(ClassLoader classLoader, Class<?> clazz, JavaClass javaClass,
            Set<String> cachedClasses, String pathPrefix) {
        Set<String> existing = javaClass.getJavaFields().getJavaField().stream()
            .map(JavaField::getName).collect(Collectors.toSet());
        Field[] fields = clazz.getDeclaredFields();
        if (fields != null && !javaClass.isEnumeration()) {
            for (Field f : fields) {
                JavaField s = inspectField(classLoader, f, cachedClasses, pathPrefix);

                if (existing.contains(f.getName())) {
                    LOG.warn("Ignoring hidden Java field: " + s.getName());
                    continue;
                }
                if (getFieldExclusions().contains(f.getName())) {
                    s.setStatus(FieldStatus.EXCLUDED);
                }

                // skip synthetic members
                if (s.isSynthetic() != null && s.isSynthetic()) {
                    if (LOG.isDebugEnabled()) {
                        LOG.debug("Synthetic field class detected: " + s.getName());
                    }
                    continue;
                }

                if (s.getGetMethod() == null && s.getSetMethod() == null) {
                    if (s.getModifiers().getModifier().contains(io.atlasmap.java.v2.Modifier.PRIVATE)
                            && !getDisablePrivateOnlyFields()) {
                        javaClass.getJavaFields().getJavaField().add(s);
                    } else if (s.getModifiers().getModifier().contains(io.atlasmap.java.v2.Modifier.PROTECTED)
                            && !getDisableProtectedOnlyFields()) {
                        javaClass.getJavaFields().getJavaField().add(s);
                    } else if (s.getModifiers().getModifier().contains(io.atlasmap.java.v2.Modifier.PUBLIC)
                            && !getDisablePublicOnlyFields()) {
                        javaClass.getJavaFields().getJavaField().add(s);
                    } else if (s.getModifiers().getModifier().contains(io.atlasmap.java.v2.Modifier.PACKAGE_PRIVATE)
                            && !getDisablePackagePrivateOnlyFields()) {
                        javaClass.getJavaFields().getJavaField().add(s);
                    }
                } else if (!getDisablePublicGetterSetterFields()) {
                    javaClass.getJavaFields().getJavaField().add(s);
                }
            }
        }
    }

    private void inspectClassMethods(ClassLoader classLoader, Class<?> clazz, JavaClass javaClass,
            Set<String> cachedClasses, String pathPrefix) {
        Method[] methods = clazz.getDeclaredMethods();
        if (methods != null && !javaClass.isEnumeration()) {
            for (Method m : methods) {
                JavaField s = AtlasJavaModelFactory.createJavaField();
                s.setName(m.getName());
                s.setSynthetic(m.isSynthetic());

                if (m.isVarArgs() || m.isBridge() || m.isSynthetic() || m.isDefault()) {
                    s.setStatus(FieldStatus.UNSUPPORTED);
                    LOG.warn("VarArg, Bridge, Synthetic or Default method " + m.getName() + " detected");
                    continue;
                }
                s.setSynthetic(m.isSynthetic());

                if (m.getName().startsWith("get") || m.getName().startsWith("is")) {
                    s = inspectGetMethod(classLoader, m, s, cachedClasses, pathPrefix);
                }

                if (m.getName().startsWith("set")) {
                    s = inspectSetMethod(classLoader, m, s, cachedClasses, pathPrefix);
                }

                boolean found = false;
                for (int i = 0; i < javaClass.getJavaFields().getJavaField().size(); i++) {
                    JavaField exists = javaClass.getJavaFields().getJavaField().get(i);
                    if (s.getName().equals(exists.getName())) {
                        found = true;

                        // Merge get/set method info for interfaces that don't
                        // have fields
                        if (exists.getGetMethod() == null && s.getGetMethod() != null) {
                            exists.setGetMethod(s.getGetMethod());
                        }
                        if (exists.getSetMethod() == null && s.getSetMethod() != null) {
                            exists.setSetMethod(s.getSetMethod());
                        }
                    }
                }

                if (found) {
                    if (LOG.isTraceEnabled()) {
                        LOG.trace("Field already defined for method: " + m.getName() + " class: " + clazz.getName());
                    }
                } else if (s.getGetMethod() != null || s.getSetMethod() != null) {
                    javaClass.getJavaFields().getJavaField().add(s);
                } else {
                    if (LOG.isTraceEnabled()) {
                        LOG.trace("Ignoring non-field method: " + m.getName() + " class: " + clazz.getName());
                    }
                }
            }
        }
    }

    protected boolean isFieldMap(String fieldType) {
        return getMapClasses().contains(fieldType);
    }

    private Integer detectArrayDimensions(Class<?> clazz) {
        Integer arrayDim = Integer.valueOf(0);
        if (clazz == null) {
            return null;
        }

        if (!clazz.isArray()) {
            return arrayDim;
        }
        arrayDim++;

        Class<?> tmpClazz = clazz.getComponentType();
        while (tmpClazz != null && tmpClazz.isArray() && arrayDim < MAX_ARRAY_DIM_LIMIT) {
            arrayDim++;
            tmpClazz = tmpClazz.getComponentType();
        }
        return arrayDim;
    }

    private List<io.atlasmap.java.v2.Modifier> detectModifiers(int m) {
        List<io.atlasmap.java.v2.Modifier> modifiers = new ArrayList<>();
        if (Modifier.isAbstract(m)) {
            modifiers.add(io.atlasmap.java.v2.Modifier.ABSTRACT);
        }
        if (Modifier.isFinal(m)) {
            modifiers.add(io.atlasmap.java.v2.Modifier.FINAL);
        }
        if (Modifier.isInterface(m)) {
            modifiers.add(io.atlasmap.java.v2.Modifier.INTERFACE);
        }
        if (Modifier.isNative(m)) {
            modifiers.add(io.atlasmap.java.v2.Modifier.NATIVE);
        }
        if (Modifier.isPrivate(m)) {
            modifiers.add(io.atlasmap.java.v2.Modifier.PRIVATE);
        }
        if (Modifier.isProtected(m)) {
            modifiers.add(io.atlasmap.java.v2.Modifier.PROTECTED);
        }
        if (Modifier.isPublic(m)) {
            modifiers.add(io.atlasmap.java.v2.Modifier.PUBLIC);
        }
        if (!Modifier.isPrivate(m) && !Modifier.isProtected(m) && !Modifier.isPublic(m)) {
            modifiers.add(io.atlasmap.java.v2.Modifier.PACKAGE_PRIVATE);
        }
        if (Modifier.isStatic(m)) {
            modifiers.add(io.atlasmap.java.v2.Modifier.STATIC);
        }
        if (Modifier.isStrict(m)) {
            modifiers.add(io.atlasmap.java.v2.Modifier.STRICT);
        }
        if (Modifier.isSynchronized(m)) {
            modifiers.add(io.atlasmap.java.v2.Modifier.SYNCHRONIZED);
        }
        if (Modifier.isTransient(m)) {
            modifiers.add(io.atlasmap.java.v2.Modifier.TRANSIENT);
        }
        if (Modifier.isVolatile(m)) {
            modifiers.add(io.atlasmap.java.v2.Modifier.VOLATILE);
        }
        return modifiers;
    }

    private Class<?> detectListClass(ClassLoader classLoader, Field field) throws ClassNotFoundException {
        List<String> types = detectParameterizedTypes(field, true);
        if (types != null && !types.isEmpty()) {
            return classLoader.loadClass(types.get(0));
        }
        return null;
    }

    private Class<?> detectListClassFromMethodReturn(Method m) {
        return ClassHelper.detectClassFromTypeArgument(m.getGenericReturnType());
    }

    private Class<?> detectListClassFromMethodParameter(Method m) {
        return ClassHelper.detectClassFromTypeArgument(m.getGenericParameterTypes()[0]);
    }

    private Class<?> detectArrayClass(Class<?> clazz) {
        Integer arrayDim = new Integer(0);
        if (clazz == null) {
            return null;
        }

        if (!clazz.isArray()) {
            return clazz;
        }
        arrayDim++;

        Class<?> tmpClazz = clazz.getComponentType();
        while (tmpClazz != null && tmpClazz.isArray() && arrayDim < MAX_ARRAY_DIM_LIMIT) {
            arrayDim++;
            tmpClazz = tmpClazz.getComponentType();
        }
        return tmpClazz;
    }

    private List<String> detectParameterizedTypes(Field field, boolean onlyClasses) {
        List<String> pTypes = null;

        if (field == null || field.getGenericType() == null || !(field.getGenericType() instanceof ParameterizedType)) {
            return null;
        }
        Type[] types = ((ParameterizedType) field.getGenericType()).getActualTypeArguments();
        if (types.length == 0) {
            return null;
        }

        for (Type t : types) {
            if (pTypes == null) {
                pTypes = new ArrayList<>();
            }

            if (!onlyClasses && t instanceof TypeVariable) {
                TypeVariable<?> tv = (TypeVariable<?>) t;
                // TODO: no current need, but we may want to have treatment for 'T'
                // tv.getTypeName()
                Type type = tv.getAnnotatedBounds()[0].getType();
                if (type instanceof Class) {
                    pTypes.add(((Class<?>) type).getName());
                } else {
                    pTypes.add(type.getTypeName());
                }
            }

            if (!onlyClasses && t instanceof WildcardType) {
                WildcardType wc = (WildcardType) t;
                Type[] upperBounds = wc.getUpperBounds();
                Type[] lowerBounds = wc.getLowerBounds();
                // TODO: No current need, but we may want to have treatment for '?'
                // wc.getTypeName()
                if (upperBounds != null && upperBounds.length > 0) {
                    pTypes.add(wc.getUpperBounds()[0].getClass().getName());
                } else if (lowerBounds != null && lowerBounds.length > 0) {
                    pTypes.add(wc.getLowerBounds()[0].getClass().getName());
                }
            }

            if (t instanceof Class) {
                pTypes.add(((Class<?>) t).getName());
            }
        }
        return pTypes;
    }

    private JavaClass convertJavaFieldToJavaClass(JavaField javaField) {
        JavaClass javaClass = AtlasJavaModelFactory.createJavaClass();
        javaClass.setArrayDimensions(javaField.getArrayDimensions());
        javaClass.setArraySize(javaField.getArraySize());
        javaClass.setCollectionClassName(javaField.getCollectionClassName());
        javaClass.setCollectionType(javaField.getCollectionType());
        javaClass.setDocId(javaField.getDocId());
        javaClass.setPrimitive(javaField.isPrimitive());
        javaClass.setSynthetic(javaField.isSynthetic());
        javaClass.setClassName(javaField.getClassName());
        javaClass.setGetMethod(javaField.getGetMethod());
        javaClass.setName(javaField.getName());
        javaClass.setPath(javaField.getPath());
        javaClass.setRequired(javaField.isRequired());
        javaClass.setSetMethod(javaField.getSetMethod());
        javaClass.setStatus(javaField.getStatus());
        javaClass.setFieldType(javaField.getFieldType());
        if (javaField.getClassName() != null) {
            javaClass.setUri(String.format(AtlasJavaModelFactory.URI_FORMAT, AtlasUtil.escapeForUri(javaField.getClassName())));
        }
        javaClass.setValue(javaField.getValue());
        javaClass.setAnnotations(javaField.getAnnotations());
        javaClass.setModifiers(javaField.getModifiers());
        javaClass.setParameterizedTypes(javaField.getParameterizedTypes());
        return javaClass;
    }

    private AtlasConversionService getConversionService() {
        return atlasConversionService;
    }

    public void setConversionService(AtlasConversionService atlasConversionService) {
        this.atlasConversionService = atlasConversionService;
    }

}