All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.nedap.archie.rminfo.ReflectionModelInfoLookup Maven / Gradle / Ivy

package com.nedap.archie.rminfo;

import com.google.common.reflect.TypeToken;
import com.nedap.archie.aom.CPrimitiveObject;
import org.reflections.ReflectionUtils;
import org.reflections.Reflections;
import org.reflections.scanners.Scanners;
import org.reflections.util.ClasspathHelper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.annotation.Nullable;
import java.lang.annotation.Annotation;
import java.lang.reflect.*;
import java.util.*;
import java.util.stream.Collectors;

/**
 * Utility that defines the java mapping of type and attribute names of a given reference model.
 *
 * Use it to obtain java classes for RM Type Names, and java fields, getters, setters and types for RM Attribute Names
 *
 * This class is never directly created, but subclasses must be created that setup the correct model. Create a subclass
 * per model you want to use with Archie, for example one for an OpenEHR RM implementation, or the CIMI RM implementation
 *
 * Created by pieter.bos on 02/02/16.
 */
public abstract class ReflectionModelInfoLookup implements ModelInfoLookup {

    private static final Logger logger = LoggerFactory.getLogger(ReflectionModelInfoLookup.class);

    private ModelNamingStrategy namingStrategy;

    private String packageName;
    private ClassLoader classLoader;

    private Map rmTypeNamesToRmTypeInfo = new HashMap<>();
    private Map, RMTypeInfo> classesToRmTypeInfo = new HashMap<>();

    private boolean inConstructor = true;
    private boolean addAttributesWithoutField = true;

    /**
     * All methods that cannot be called by using reflection. For example getClass();
     */
    private Set forbiddenMethods = new HashSet<>(
        Arrays.asList("getClass", "wait", "notify", "notifyAll", "clone", "finalize")
    );

    public ReflectionModelInfoLookup(ModelNamingStrategy namingStrategy, Class baseClass) {
        this(namingStrategy, baseClass, ReflectionModelInfoLookup.class.getClassLoader(), true);
    }

    public ReflectionModelInfoLookup(ModelNamingStrategy namingStrategy, String packageName, ClassLoader classLoader) {
        this.packageName = packageName;
        this.namingStrategy = namingStrategy;

        this.classLoader = classLoader;
        Reflections reflections = new Reflections(packageName, Scanners.SubTypes.filterResultsBy(s -> true));
        Set typeNames = reflections.getAllTypes();

        typeNames.forEach(typeName -> {
            try {
                addClass(classLoader.loadClass(typeName));
            } catch (ClassNotFoundException e) {
                logger.error("error loading model info lookup", e);
            }
        });
        addSuperAndSubclassInfo();
        addAlternativeTypeNames();
        inConstructor = false;
    }

    public ReflectionModelInfoLookup(ModelNamingStrategy namingStrategy, Class baseClass, ClassLoader classLoader, boolean addAttributesWithoutField) {
        this.namingStrategy = namingStrategy;
        this.addAttributesWithoutField = addAttributesWithoutField;

        this.classLoader = classLoader;
        addTypes(baseClass);
        addSuperAndSubclassInfo();
        addAlternativeTypeNames();
        inConstructor = false;
    }

    /**
     * Adds bindings to alternative type names, for parsing backwards compatible JSON with
     * old incompatible type names
     */
    private void addAlternativeTypeNames() {
        for(Class clazz:this.classesToRmTypeInfo.keySet()) {
            for(String alternativeName:namingStrategy.getAlternativeTypeNames(clazz)) {
                String originalName = namingStrategy.getTypeName(clazz);
                this.rmTypeNamesToRmTypeInfo.put(alternativeName, rmTypeNamesToRmTypeInfo.get(originalName));
            }
        }
    }

    /**
     * Override to disable reflections scanning
     * @param baseClass
     */
    protected void addTypes(Class baseClass) {
        addSubtypesOf(baseClass);
    }

    private void addSuperAndSubclassInfo() {
        for(RMTypeInfo typeInfo:rmTypeNamesToRmTypeInfo.values()) {
            Class superclass = typeInfo.getJavaClass().getSuperclass();
            if(!superclass.equals(Object.class)) {
                addDescendantClass(typeInfo, superclass);
            }

            for(Class interfaceClass:typeInfo.getJavaClass().getInterfaces()) {
                addDescendantClass(typeInfo, interfaceClass);
            }
        }

    }

    private void addDescendantClass(RMTypeInfo typeInfo, Class interfaceClass) {
        RMTypeInfo superClassTypeInfo = this.getTypeInfo(interfaceClass);
        if(superClassTypeInfo != null) {
            typeInfo.addDirectParentClass(superClassTypeInfo);
            superClassTypeInfo.addDirectDescendantClass(typeInfo);
        }
    }

    /**
     * Add all subtypes of the given class
     * @param baseClass
     */
    protected  void addSubtypesOf(Class baseClass) {
        Reflections reflections = new Reflections(ClasspathHelper.forClass(baseClass), Scanners.SubTypes.filterResultsBy(s -> true));
        Set> classes = reflections.getSubTypesOf(baseClass);

        classes.forEach(this::addClass);
        addClass(baseClass);
    }

    protected void addClass(Class clazz) {
        String rmTypeName = namingStrategy.getTypeName(clazz);
        RMTypeInfo typeInfo = new RMTypeInfo(clazz, rmTypeName);
        addAttributeInfo(clazz, typeInfo);
        addInvariantChecks(clazz, typeInfo);
        rmTypeNamesToRmTypeInfo.put(rmTypeName, typeInfo);
        classesToRmTypeInfo.put(clazz, typeInfo);
        if(!inConstructor) {
            //if someone called this after initial creation, we need to update super/subclass info.
            //could be done more efficiently by only updating for the added class and parents/descendants, but
            //should not be a problem to do it this way
            addSuperAndSubclassInfo();
            addAlternativeTypeNames();
        }
    }

    private void addAttributeInfo(Class clazz, RMTypeInfo typeInfo) {
        //TODO: it's possible to constrain some method as well. should we do that here too?
        TypeToken typeToken = TypeToken.of(clazz);

        Set allFields = ReflectionUtils.getAllFields(clazz);
        Map fieldsByName = allFields.stream()
                .filter( field -> !field.getName().startsWith("$")) //jacoco adds $ fields.. annoying :)
                .collect(Collectors.toMap((field) -> field.getName(), (field) -> field,
                        (duplicate1, duplicate2) -> duplicate1));
        for(Field field: fieldsByName.values()) {
            addRMAttributeInfo(clazz, typeInfo, typeToken, field);
        }
        if(addAttributesWithoutField) {
            Set getters = ReflectionUtils.getAllMethods(clazz, (method) -> method.getName().startsWith("get") || method.getName().startsWith("is"));
            Map gettersByName = getters.stream()
                    .filter(this::shouldAdd)
                    // Only use the most specific method for each name
                    .collect(Collectors.toMap(Method::getName, method -> method, new SpecificMethodSelector()));
            for (Method getMethod : gettersByName.values()) {
                addRMAttributeInfo(clazz, typeInfo, typeToken, getMethod, fieldsByName);
            }
        }
    }

    private void addInvariantChecks(Class clazz, RMTypeInfo typeInfo) {
        Set allInvariants = ReflectionUtils.getAllMethods(clazz, (method) -> method.getAnnotation(Invariant.class) != null);
        for(Method method:allInvariants) {
            if(method.getParameterCount() != 0) {
                throw new RuntimeException("An invariant check must not have any parameters, in method " + clazz.getSimpleName() + "::" + method.getName());
            }
            Class returnType = method.getReturnType();
            if(!(returnType.equals(Boolean.class) || returnType.equals(boolean.class))) {
                throw new RuntimeException("An invariant check must return a boolean parameter, was " + returnType.getSimpleName() + " in method " + clazz.getSimpleName() + "::" + method.getName());
            }
            Invariant annotation = method.getAnnotation(Invariant.class);
            typeInfo.addInvariantMethod(method, annotation);
        }

    }

    protected boolean shouldAdd(Method method) {
        if(method == null) {
            return true;
        }
        //do not add invariants
        if(method.getAnnotation(Invariant.class) != null) {
            return false;
        }
        //do not add private or protected properties, they will result in errors
        return Modifier.isPublic(method.getModifiers()) && method.getAnnotation(RMPropertyIgnore.class) == null;
    }

    protected void addRMAttributeInfo(Class clazz, RMTypeInfo typeInfo, TypeToken typeToken, Method getMethod, Map fieldsByName) {
        String javaFieldName = null;
        if(getMethod.getName().startsWith("is")) {
            javaFieldName = lowerCaseFirstChar(getMethod.getName().substring(2));
        } else {
            javaFieldName = lowerCaseFirstChar(getMethod.getName().substring(3));
        }
        Field field = fieldsByName.get(javaFieldName);
        if(field == null) {
            field = fieldsByName.get(getMethod.getName());
        }
        String javaFieldNameUpperCased = upperCaseFirstChar(javaFieldName);
        Method setMethod = null, addMethod = null;

        if (getMethod == null) {
            getMethod = getMethod(clazz, "is" + javaFieldNameUpperCased);
        }
        if (getMethod != null) {
            setMethod = getMethod(clazz, "set" + javaFieldNameUpperCased, getMethod.getReturnType());
            addMethod = getAddMethod(clazz, typeToken, javaFieldNameUpperCased, getMethod);
        } else {
            logger.debug("No get method found for attribute {} on class {}", javaFieldName, clazz.getSimpleName());
        }

        String attributeName = namingStrategy.getAttributeName(field, getMethod);

        TypeToken fieldType = typeToken.resolveType(getMethod.getGenericReturnType());;

        Class rawFieldType = fieldType.getRawType();
        Class typeInCollection = getTypeInCollection(fieldType);
       // if (setMethod != null) {

            RMAttributeInfo attributeInfo = new RMAttributeInfo(
                    attributeName,
                    field,
                    rawFieldType,
                    typeInCollection,
                    this.namingStrategy.getTypeName(typeInCollection),
                    isNullable(clazz, getMethod, field),
                    getMethod,
                    setMethod,
                    addMethod,
                    determineIfComputed(clazz, getMethod, field, setMethod, addMethod)
            );
            if(typeInfo.getAttribute(attributeName) == null) {
                typeInfo.addAttribute(attributeInfo);
            }
        //} else {
        //    logger.info("property without a set method ignored for field {} on class {}", attributeName, clazz.getSimpleName());
       // }
    }

    private boolean determineIfComputed(Class clazz, Method getMethod, Field field, Method setMethod, Method addMethod) {
        boolean computed = setMethod == null && addMethod == null && field == null;

        RMProperty annotation = getAnnotation(clazz, getMethod, field, RMProperty.class);
        if(annotation != null && annotation.computed() != PropertyType.AUTO_DETECT) {
            computed = annotation.computed() == PropertyType.COMPUTED;
        }
        return computed;
    }

    protected boolean isNullable(Class clazz, Method getMethod, Field field) {
        return getAnnotation(clazz, getMethod, field, Nullable.class) != null;
    }

    private  T getAnnotation(Class clazz, Method getMethod, Field field, Class annotationClass) {
        if(field != null) {
            T annotation = (T) field.getAnnotation(annotationClass);
            if(annotation != null) {
                return annotation;
            }
        }
        if(getMethod != null) {
            T annotation = (T) getMethod.getAnnotation(annotationClass);
            if(annotation != null) {
                return annotation;
            }
        }
        return null;
    }


    private void addRMAttributeInfo(Class clazz, RMTypeInfo typeInfo, TypeToken typeToken, Field field) {
        String javaFieldName = field.getName();
        String javaFieldNameUpperCased = upperCaseFirstChar(javaFieldName);
        Method getMethod = getMethod(clazz, "get" + javaFieldNameUpperCased);
        Method setMethod = null, addMethod = null;
        if (getMethod == null) {
            getMethod = getMethod(clazz, "is" + javaFieldNameUpperCased);
        }
        if (getMethod != null) {
            setMethod = getMethod(clazz, "set" + javaFieldNameUpperCased, getMethod.getReturnType());
            addMethod = getAddMethod(clazz, typeToken, javaFieldNameUpperCased, getMethod);
        } else {
            logger.debug("No get method found for field {} on class {}", field.getName(), clazz.getSimpleName());
        }

        if(javaFieldName.startsWith("is")) {
            //special case
            String fieldNameWithoutPrefix = javaFieldName.substring(2);
            String withoutPrefixUpperCased = upperCaseFirstChar(fieldNameWithoutPrefix);
            if (getMethod == null) {
                getMethod = getMethod(clazz, "is" + withoutPrefixUpperCased);
            }
            if (getMethod != null) {
                if(setMethod == null) {
                    setMethod = getMethod(clazz, "set" + withoutPrefixUpperCased, getMethod.getReturnType());
                }
                if(addMethod == null) {
                    addMethod = getAddMethod(clazz, typeToken, withoutPrefixUpperCased, getMethod);
                }
            } else {
                logger.debug("No get method found for attribute {} on class {}", javaFieldName, clazz.getSimpleName());
            }

        }
        String attributeName = namingStrategy.getAttributeName(field, getMethod);

        TypeToken fieldType = null;
        if (getMethod != null) {
            fieldType = typeToken.resolveType(getMethod.getGenericReturnType());
        } else {
            fieldType = typeToken.resolveType(field.getGenericType());
        }

        Class rawFieldType = fieldType.getRawType();
        Class typeInCollection = getTypeInCollection(fieldType);
        if (setMethod != null && (shouldAdd(setMethod) && shouldAdd(getMethod))) {
            RMAttributeInfo attributeInfo = new RMAttributeInfo(
                    attributeName,
                    field,
                    rawFieldType,
                    typeInCollection,
                    namingStrategy.getTypeName(typeInCollection),
                    isNullable(clazz, getMethod, field),
                    getMethod,
                    setMethod,
                    addMethod,
                    determineIfComputed(clazz, getMethod, field, setMethod, addMethod)
            );
            typeInfo.addAttribute(attributeInfo);
        } else {
            logger.debug("property without a set method ignored for field {} on class {}", field.getName(), clazz.getSimpleName());
        }
    }

    private Class getTypeInCollection(TypeToken fieldType) {
        Class rawFieldType = fieldType.getRawType();
        if (Collection.class.isAssignableFrom(rawFieldType)) {
            Type[] actualTypeArguments = ((ParameterizedType) fieldType.getType()).getActualTypeArguments();
            if (actualTypeArguments.length == 1) {
                //the java reflection api is kind of tricky with types. This works for the archie RM, but may cause problems for other RMs. The fix is implementing more ways
                if (actualTypeArguments[0] instanceof Class) {
                    return (Class) actualTypeArguments[0];
                } else if (actualTypeArguments[0] instanceof ParameterizedType) {
                    ParameterizedType parameterizedTypeInCollection = (ParameterizedType) actualTypeArguments[0];
                    return (Class) parameterizedTypeInCollection.getRawType();
                } else if (actualTypeArguments[0] instanceof java.lang.reflect.TypeVariable) {
                    return (Class) ((java.lang.reflect.TypeVariable) actualTypeArguments[0]).getBounds()[0];
                }
            }
        } else if(rawFieldType.isArray()) {
           return rawFieldType.getComponentType();
        }
        return rawFieldType;
    }

    private Method getAddMethod(Class clazz, TypeToken typeToken, String javaFieldNameUpperCased, Method getMethod) {
        Method addMethod = null;
        if (Collection.class.isAssignableFrom(getMethod.getReturnType())) {
            Type[] typeArguments = ((ParameterizedType) getMethod.getGenericReturnType()).getActualTypeArguments();
            if (typeArguments.length == 1) {
                TypeToken singularParameter = typeToken.resolveType(typeArguments[0]);
                //TODO: does this work or should we use the typeArguments[0].getSomething?
                String addMethodName = "add" + toSingular(javaFieldNameUpperCased);
                addMethod = getMethod(clazz, addMethodName, singularParameter.getRawType());
                if (addMethod == null) {
                    //Due to generics, this does not always work
                    Set allAddMethods = ReflectionUtils.getAllMethods(clazz, ReflectionUtils.withName(addMethodName));
                    if (allAddMethods.size() == 1) {
                        addMethod = allAddMethods.iterator().next();
                    } else {
                        logger.debug("strange number of add methods for field {} on class {}", javaFieldNameUpperCased, clazz.getSimpleName());
                    }
                }
            }
        }
        return addMethod;
    }

    private String toSingular(String javaFieldNameUpperCased) {
        if(javaFieldNameUpperCased.endsWith("s")) {
            return javaFieldNameUpperCased.substring(0, javaFieldNameUpperCased.length() - 1);
        }
        //TODO: a way to override plural names to go back to singular names. Use a library?
        return javaFieldNameUpperCased;
    }

    private Method getMethod(Class clazz, String name, Class... parameterTypes) {
        try {
            return clazz.getMethod(name, parameterTypes);
        } catch(NoSuchMethodException ex) {
            return null;
        }
    }

    private String upperCaseFirstChar(String name) {
        return new StringBuilder(name).replace(0,1,
                Character.toString(Character.toUpperCase(name.charAt(0)))
            ).toString();
    }

    private String lowerCaseFirstChar(String name) {
        return new StringBuilder(name).replace(0,1,
                Character.toString(Character.toLowerCase(name.charAt(0)))
        ).toString();
    }

    @Override
    public Class getClass(String rmTypeName) {
        String strippedRmTypeName = getTypeWithoutGenericType(rmTypeName);
        RMTypeInfo rmTypeInfo = rmTypeNamesToRmTypeInfo.get(strippedRmTypeName);
        return rmTypeInfo == null ? null : rmTypeInfo.getJavaClass();
    }

    @Override
    public Class getClassToBeCreated(String rmTypename) {
        return getClass(rmTypename);
    }

    @Override
    public Map> getRmTypeNameToClassMap() {
        HashMap> result = new HashMap<>();
        for(String rmTypeName: rmTypeNamesToRmTypeInfo.keySet()) {
            result.put(rmTypeName, rmTypeNamesToRmTypeInfo.get(rmTypeName).getJavaClass());
        }
        return result;
    }

    @Override
    public RMTypeInfo getTypeInfo(Class clazz) {
        return this.classesToRmTypeInfo.get(clazz);
    }

    @Override
    public Field getField(Class clazz, String attributeName) {
        RMTypeInfo typeInfo = classesToRmTypeInfo.get(clazz);
        RMAttributeInfo attributeInfo = typeInfo == null ? null : typeInfo.getAttribute(attributeName);
        return attributeInfo == null ? null : attributeInfo.getField();
    }

    @Override
    public RMTypeInfo   getTypeInfo(String rmTypeName) {
        String strippedRmTypeName = getTypeWithoutGenericType(rmTypeName);
        return this.rmTypeNamesToRmTypeInfo.get(strippedRmTypeName);
    }

    private String getTypeWithoutGenericType(String rmTypeName) {
        if(rmTypeName.indexOf('<') > 0) {
            //strip generic types, cannot handle them yet
            rmTypeName = rmTypeName.substring(0, rmTypeName.indexOf('<'));
        }
        return rmTypeName;
    }

    @Override
    public RMAttributeInfo getAttributeInfo(Class clazz, String attributeName) {
        RMTypeInfo typeInfo = this.classesToRmTypeInfo.get(clazz);
        return typeInfo == null ? null : typeInfo.getAttribute(attributeName);
    }

    @Override
    public RMAttributeInfo getAttributeInfo(String rmTypeName, String attributeName) {
        String strippedRmTypeName = getTypeWithoutGenericType(rmTypeName);
        RMTypeInfo typeInfo = this.rmTypeNamesToRmTypeInfo.get(strippedRmTypeName);
        return typeInfo == null ? null : typeInfo.getAttribute(attributeName);
    }

    @Override
    public List getAllTypes() {
        return new ArrayList<>(classesToRmTypeInfo.values());
    }

    @Override
    public ModelNamingStrategy getNamingStrategy() {
        return namingStrategy;
    }

    /**
     * Convert the given reference model object to the object required for the archetype constraint.
     *
     * for example, a CTerminologyCode can be used to check a CodePhrase or a DvCodedText. This cannot be directly checked and must be converted first.
     *
     * @param object
     * @param cPrimitiveObject
     * @return
     */
    @Override
    public Object convertToConstraintObject(Object object, CPrimitiveObject cPrimitiveObject) {
        return object;
    }

    @Override
    public Object convertConstrainedPrimitiveToRMObject(Object object) {
        //TODO: this should take an AttributeInfo as param, so to be able to pick the right object
        return object;
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy