com.haulmont.chile.core.loader.ChileAnnotationsLoader.java Source code

Java tutorial

Introduction

Here is the source code for com.haulmont.chile.core.loader.ChileAnnotationsLoader.java

Source

/*
 * Copyright (c) 2008-2016 Haulmont.
 *
 * 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 com.haulmont.chile.core.loader;

import com.google.common.base.Joiner;
import com.haulmont.bali.util.ReflectionHelper;
import com.haulmont.chile.core.annotations.Composition;
import com.haulmont.chile.core.datatypes.Datatype;
import com.haulmont.chile.core.datatypes.Datatypes;
import com.haulmont.chile.core.datatypes.impl.EnumerationImpl;
import com.haulmont.chile.core.model.*;
import com.haulmont.chile.core.model.impl.*;
import com.haulmont.cuba.core.entity.annotation.IgnoreUserTimeZone;
import com.haulmont.cuba.core.entity.annotation.SystemLevel;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.core.io.support.ResourcePatternResolver;
import org.springframework.core.type.AnnotationMetadata;
import org.springframework.core.type.ClassMetadata;
import org.springframework.core.type.classreading.CachingMetadataReaderFactory;
import org.springframework.core.type.classreading.MetadataReader;
import org.springframework.core.type.classreading.MetadataReaderFactory;

import javax.annotation.Nullable;
import java.io.IOException;
import java.lang.reflect.*;
import java.util.*;

public class ChileAnnotationsLoader implements MetaClassLoader {

    private Logger log = LoggerFactory.getLogger(ChileAnnotationsLoader.class);

    protected Session session;

    protected ResourcePatternResolver resourcePatternResolver = new PathMatchingResourcePatternResolver();
    protected MetadataReaderFactory metadataReaderFactory = new CachingMetadataReaderFactory(
            this.resourcePatternResolver);

    public ChileAnnotationsLoader(Session session) {
        this.session = session;
    }

    @Override
    public void loadPackage(String packageName, List<String> classNames) {
        List<Class<?>> classes;
        List<MetadataObjectInitTask> tasks = new ArrayList<>();

        if (classNames != null) {
            classes = new ArrayList<>();
            for (String className : classNames) {
                try {
                    classes.add(ReflectionHelper.loadClass(className));
                } catch (ClassNotFoundException e) {
                    log.warn("Class " + className + " not found for model " + packageName);
                }
            }
        } else {
            String packagePrefix = packageName.replace(".", "/") + "/**/*.class";
            String packageSearchPath = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX + packagePrefix;
            Resource[] resources;
            try {
                resources = resourcePatternResolver.getResources(packageSearchPath);
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
            classes = getClasses(resources);
        }

        for (Class<?> aClass : classes) {
            if (aClass.getName().startsWith(packageName)) {
                MetadataObjectInfo<MetaClass> info = loadClass(packageName, aClass);
                if (info != null) {
                    tasks.addAll(info.getTasks());
                } else if (classNames != null) {
                    // If the class name is specified explicitly, print a warning
                    log.warn("Class " + aClass.getName() + " is not loaded into metadata");
                }
            }
        }

        for (MetadataObjectInitTask task : tasks) {
            task.execute();
        }
    }

    protected List<Class<?>> getClasses(Resource[] resources) {
        List<Class<?>> annotated = new ArrayList<>();

        for (Resource resource : resources) {
            if (resource.isReadable()) {
                MetadataReader metadataReader;
                try {
                    metadataReader = metadataReaderFactory.getMetadataReader(resource);
                } catch (IOException e) {
                    throw new RuntimeException("Unable to read metadata resource", e);
                }

                AnnotationMetadata annotationMetadata = metadataReader.getAnnotationMetadata();
                if (annotationMetadata.isAnnotated(com.haulmont.chile.core.annotations.MetaClass.class.getName())) {
                    ClassMetadata classMetadata = metadataReader.getClassMetadata();
                    Class c = ReflectionHelper.getClass(classMetadata.getClassName());
                    annotated.add(c);
                }
            }
        }

        return annotated;
    }

    protected Class getTypeOverride(AnnotatedElement element) {
        return null;
    }

    protected boolean isMandatory(Field field) {
        com.haulmont.chile.core.annotations.MetaProperty metaPropertyAnnotation = field
                .getAnnotation(com.haulmont.chile.core.annotations.MetaProperty.class);
        return metaPropertyAnnotation != null && metaPropertyAnnotation.mandatory();
    }

    protected boolean isMetaPropertyField(Field field) {
        return field.isAnnotationPresent(com.haulmont.chile.core.annotations.MetaProperty.class);
    }

    protected boolean isMetaPropertyMethod(Method method) {
        return method.isAnnotationPresent(com.haulmont.chile.core.annotations.MetaProperty.class);
    }

    protected MetaClassImpl createMetaClass(String packageName, String className) {
        MetaModel model = session.getModel(packageName);
        if (model == null) {
            model = new MetaModelImpl(session, packageName);
        }
        return new MetaClassImpl(model, className);
    }

    @Nullable
    protected MetaClassImpl createClass(Class<?> clazz, String packageName) {
        if (Object.class.equals(clazz))
            return null;

        final com.haulmont.chile.core.annotations.MetaClass metaClassAnnotaion = clazz
                .getAnnotation(com.haulmont.chile.core.annotations.MetaClass.class);

        if (metaClassAnnotaion == null) {
            log.trace(String.format("Class %s isn't annotated as metadata entity, ignore it", clazz.getName()));
            return null;
        }

        String className = metaClassAnnotaion.name();
        if (StringUtils.isEmpty(className)) {
            className = clazz.getSimpleName();
        }

        return createClass(clazz, packageName, className);
    }

    protected boolean isCollection(Field field) {
        final Class<?> type = field.getType();
        return Collection.class.isAssignableFrom(type);
    }

    protected boolean isMap(Field field) {
        final Class<?> type = field.getType();
        return Map.class.isAssignableFrom(type);
    }

    protected boolean isMap(Method method) {
        final Class<?> type = method.getReturnType();
        return Map.class.isAssignableFrom(type);
    }

    protected boolean isCollection(Method method) {
        final Class<?> type = method.getReturnType();
        return Collection.class.isAssignableFrom(type);
    }

    @Nullable
    protected MetadataObjectInfo<MetaClass> loadClass(String packageName, Class<?> clazz) {
        final MetaClassImpl metaClass = createClass(clazz, packageName);
        if (metaClass == null)
            return null;

        Collection<MetadataObjectInitTask> tasks = new ArrayList<>();

        Collection<MetaClass> ancestors = metaClass.getAncestors();
        for (MetaClass ancestor : ancestors) {
            initProperties(ancestor.getJavaClass(), ((MetaClassImpl) ancestor), tasks);
        }

        initProperties(clazz, metaClass, tasks);

        return new MetadataObjectInfo<>(metaClass, tasks);
    }

    protected void initProperties(Class<?> clazz, MetaClassImpl metaClass,
            Collection<MetadataObjectInitTask> tasks) {
        if (!metaClass.getOwnProperties().isEmpty())
            return;

        // load collection properties after non-collection in order to have all inverse properties loaded up
        ArrayList<Field> collectionProps = new ArrayList<>();
        for (Field field : clazz.getDeclaredFields()) {
            if (field.isSynthetic())
                continue;

            final String fieldName = field.getName();

            if (isMetaPropertyField(field)) {
                MetaPropertyImpl property = (MetaPropertyImpl) metaClass.getProperty(fieldName);
                if (property == null) {
                    MetadataObjectInfo<MetaProperty> info;
                    if (isCollection(field) || isMap(field)) {
                        collectionProps.add(field);
                    } else {
                        info = loadProperty(metaClass, field);
                        tasks.addAll(info.getTasks());
                        MetaProperty metaProperty = info.getObject();
                        onPropertyLoaded(metaProperty, field);
                    }
                } else {
                    log.warn("Field " + clazz.getSimpleName() + "." + field.getName()
                            + " is not included in metadata because property " + property + " already exists");
                }
            }
        }

        for (Field f : collectionProps) {
            MetadataObjectInfo<MetaProperty> info = loadCollectionProperty(metaClass, f);
            tasks.addAll(info.getTasks());
            MetaProperty metaProperty = info.getObject();
            onPropertyLoaded(metaProperty, f);
        }

        for (Method method : clazz.getDeclaredMethods()) {
            if (method.isSynthetic())
                continue;

            String methodName = method.getName();
            if (!methodName.startsWith("get") || method.getReturnType() == void.class)
                continue;

            if (isMetaPropertyMethod(method)) {
                String name = StringUtils.uncapitalize(methodName.substring(3));

                MetaPropertyImpl property = (MetaPropertyImpl) metaClass.getProperty(name);
                if (property == null) {
                    MetadataObjectInfo<MetaProperty> info;
                    if (isCollection(method) || isMap(method)) {
                        throw new UnsupportedOperationException(
                                String.format("Method-based property %s.%s doesn't support collections and maps",
                                        clazz.getSimpleName(), method.getName()));
                    } else if (method.getParameterCount() != 0) {
                        throw new UnsupportedOperationException(
                                String.format("Method-based property %s.%s doesn't support arguments",
                                        clazz.getSimpleName(), method.getName()));
                    } else {
                        info = loadProperty(metaClass, method, name);
                        tasks.addAll(info.getTasks());
                    }
                    MetaProperty metaProperty = info.getObject();
                    onPropertyLoaded(metaProperty, method);
                } else {
                    log.warn("Method " + clazz.getSimpleName() + "." + method.getName()
                            + " is not included in metadata because property " + property + " already exists");
                }
            }
        }
    }

    ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

    protected void onPropertyLoaded(MetaProperty metaProperty, Field field) {
        loadPropertyAnnotations(metaProperty, field);
    }

    protected void onPropertyLoaded(MetaProperty metaProperty, Method method) {
        loadPropertyAnnotations(metaProperty, method);
    }

    private void loadPropertyAnnotations(MetaProperty metaProperty, AnnotatedElement annotatedElement) {
        SystemLevel systemLevel = annotatedElement.getAnnotation(SystemLevel.class);
        if (systemLevel != null) {
            metaProperty.getAnnotations().put(SystemLevel.class.getName(), systemLevel.value());
            metaProperty.getAnnotations().put(SystemLevel.class.getName() + SystemLevel.PROPAGATE,
                    systemLevel.propagate());
        }

        IgnoreUserTimeZone ignoreUserTimeZone = annotatedElement.getAnnotation(IgnoreUserTimeZone.class);
        if (ignoreUserTimeZone != null) {
            metaProperty.getAnnotations().put(IgnoreUserTimeZone.class.getName(), ignoreUserTimeZone.value());
        }

        com.haulmont.chile.core.annotations.MetaProperty metaPropertyAnnotation = annotatedElement
                .getAnnotation(com.haulmont.chile.core.annotations.MetaProperty.class);
        if (metaPropertyAnnotation != null) {
            String[] related = metaPropertyAnnotation.related();
            if (!(related.length == 1 && related[0].equals(""))) {
                metaProperty.getAnnotations().put("relatedProperties", Joiner.on(',').join(related));
            }
        }
    }

    protected void onClassLoaded(MetaClass metaClass, Class<?> clazz) {
    }

    protected MetadataObjectInfo<MetaProperty> loadProperty(MetaClassImpl metaClass, Field field) {
        Collection<MetadataObjectInitTask> tasks = new ArrayList<>();

        MetaPropertyImpl property = new MetaPropertyImpl(metaClass, field.getName());

        Range.Cardinality cardinality = getCardinality(field);
        Map<String, Object> map = new HashMap<>();
        map.put("cardinality", cardinality);
        boolean mandatory = isMandatory(field);
        map.put("mandatory", mandatory);
        Datatype datatype = getDatatype(field);
        map.put("datatype", datatype);
        String inverseField = getInverseField(field);
        if (inverseField != null)
            map.put("inverseField", inverseField);

        Class<?> type;
        Class typeOverride = getTypeOverride(field);
        if (typeOverride != null)
            type = typeOverride;
        else
            type = field.getType();

        MetadataObjectInfo<Range> info = loadRange(property, type, map);
        Range range = info.getObject();
        if (range != null) {
            ((AbstractRange) range).setCardinality(cardinality);
            property.setRange(range);
            assignPropertyType(field, property, range);
            assignInverse(property, range, inverseField);
        }
        property.setMandatory(mandatory);
        property.setReadOnly(!setterExists(field));
        property.setAnnotatedElement(field);
        property.setDeclaringClass(field.getDeclaringClass());

        if (info.getObject() != null && info.getObject().isEnum()) {
            property.setJavaType(info.getObject().asEnumeration().getJavaClass());
        } else {
            property.setJavaType(field.getType());
        }

        tasks.addAll(info.getTasks());

        return new MetadataObjectInfo<>(property, tasks);
    }

    @Nullable
    protected Datatype getDatatype(AnnotatedElement annotatedElement) {
        com.haulmont.chile.core.annotations.MetaProperty annotation = annotatedElement
                .getAnnotation(com.haulmont.chile.core.annotations.MetaProperty.class);
        return annotation != null && !annotation.datatype().equals("") ? Datatypes.get(annotation.datatype())
                : null;
    }

    protected boolean setterExists(Field field) {
        String name = "set" + StringUtils.capitalize(field.getName());
        Method[] methods = field.getDeclaringClass().getDeclaredMethods();
        for (Method method : methods) {
            if (method.getName().equals(name))
                return true;
        }
        return false;
    }

    protected MetadataObjectInfo<MetaProperty> loadProperty(MetaClassImpl metaClass, Method method, String name) {
        Collection<MetadataObjectInitTask> tasks = new ArrayList<>();

        MetaPropertyImpl property = new MetaPropertyImpl(metaClass, name);

        Map<String, Object> map = new HashMap<>();
        map.put("cardinality", Range.Cardinality.NONE);
        map.put("mandatory", false);
        Datatype datatype = getDatatype(method);
        map.put("datatype", datatype);

        Class<?> type;
        Class typeOverride = getTypeOverride(method);
        if (typeOverride != null)
            type = typeOverride;
        else
            type = method.getReturnType();

        MetadataObjectInfo<Range> info = loadRange(property, type, map);
        Range range = info.getObject();
        if (range != null) {
            ((AbstractRange) range).setCardinality(Range.Cardinality.NONE);
            property.setRange(range);
            assignPropertyType(method, property, range);
        }
        property.setMandatory(false);
        property.setReadOnly(!setterExists(method));
        property.setAnnotatedElement(method);
        property.setDeclaringClass(method.getDeclaringClass());
        property.setJavaType(method.getReturnType());

        tasks.addAll(info.getTasks());

        return new MetadataObjectInfo<>(property, tasks);
    }

    protected boolean setterExists(Method getter) {
        if (getter.getName().startsWith("get")) {
            String setterName = "set" + getter.getName().substring(3);
            Method[] methods = getter.getDeclaringClass().getDeclaredMethods();
            for (Method method : methods) {
                if (setterName.equals(method.getName())) {
                    return true;
                }
            }
        }
        return false;
    }

    protected void assignPropertyType(AnnotatedElement field, MetaProperty property, Range range) {
        if (range.isClass()) {
            Composition composition = field.getAnnotation(Composition.class);
            if (composition != null) {
                ((MetaPropertyImpl) property).setType(MetaProperty.Type.COMPOSITION);
            } else {
                ((MetaPropertyImpl) property).setType(MetaProperty.Type.ASSOCIATION);
            }
        } else if (range.isDatatype()) {
            ((MetaPropertyImpl) property).setType(MetaProperty.Type.DATATYPE);
        } else if (range.isEnum()) {
            ((MetaPropertyImpl) property).setType(MetaProperty.Type.ENUM);
        } else {
            throw new UnsupportedOperationException();
        }
    }

    @SuppressWarnings({ "unchecked" })
    protected MetadataObjectInfo<Range> loadRange(MetaProperty metaProperty, Class type, Map<String, Object> map) {
        Datatype datatype = (Datatype) map.get("datatype");
        if (datatype != null) {
            return new MetadataObjectInfo<>(new DatatypeRange(datatype));
        }

        datatype = Datatypes.get(type);
        if (datatype != null) {
            MetaClass metaClass = metaProperty.getDomain();
            Class javaClass = metaClass.getJavaClass();

            try {
                String name = "get" + StringUtils.capitalize(metaProperty.getName());
                Method method = javaClass.getMethod(name);

                Class<Enum> returnType = (Class<Enum>) method.getReturnType();
                if (returnType.isEnum()) {
                    return new MetadataObjectInfo<>(new EnumerationRange(new EnumerationImpl<>(returnType)));
                }
            } catch (NoSuchMethodException e) {
                // ignore
            }
            return new MetadataObjectInfo<>(new DatatypeRange(datatype));

        } else if (type.isEnum()) {
            return new MetadataObjectInfo<>(new EnumerationRange(new EnumerationImpl<>(type)));

        } else {
            MetaClassImpl rangeClass = (MetaClassImpl) session.getClass(type);
            if (rangeClass != null) {
                return new MetadataObjectInfo<>(new ClassRange(rangeClass));
            } else {
                return new MetadataObjectInfo<>(null,
                        Collections.singletonList(new RangeInitTask(metaProperty, type, map)));
            }
        }
    }

    protected Class getFieldType(Field field) {
        Type genericType = field.getGenericType();
        Class type;
        if (genericType instanceof ParameterizedType) {
            Type[] types = ((ParameterizedType) genericType).getActualTypeArguments();
            if (Map.class.isAssignableFrom(field.getType()))
                type = (Class<?>) types[1];
            else
                type = (Class<?>) types[0];
        } else {
            type = getFieldTypeAccordingAnnotations(field);
        }
        if (type == null)
            throw new IllegalArgumentException("Field " + field
                    + " must either be of parameterized type or have a JPA annotation declaring a targetEntity");
        return type;
    }

    protected Class getFieldTypeAccordingAnnotations(Field field) {
        throw new UnsupportedOperationException();
    }

    protected Range.Cardinality getCardinality(Field field) {
        Class<?> type = field.getType();
        if (Collection.class.isAssignableFrom(type)) {
            return Range.Cardinality.ONE_TO_MANY;
        } else if (type.isPrimitive() || type.equals(String.class) || Number.class.isAssignableFrom(type)
                || Date.class.isAssignableFrom(type) || UUID.class.isAssignableFrom(type)) {
            return Range.Cardinality.NONE;
        } else
            return Range.Cardinality.MANY_TO_ONE;
    }

    protected String getInverseField(Field field) {
        return null;
    }

    protected MetadataObjectInfo<MetaProperty> loadCollectionProperty(MetaClassImpl metaClass, Field field) {
        Collection<MetadataObjectInitTask> tasks = new ArrayList<>();

        MetaPropertyImpl property = new MetaPropertyImpl(metaClass, field.getName());

        Class type = getFieldType(field);

        Range.Cardinality cardinality = getCardinality(field);
        boolean ordered = isOrdered(field);
        boolean mandatory = isMandatory(field);
        String inverseField = getInverseField(field);

        Map<String, Object> map = new HashMap<>();
        map.put("cardinality", cardinality);
        map.put("ordered", ordered);
        map.put("mandatory", mandatory);
        if (inverseField != null)
            map.put("inverseField", inverseField);

        MetadataObjectInfo<Range> info = loadRange(property, type, map);
        Range range = info.getObject();
        if (range != null) {
            ((AbstractRange) range).setCardinality(cardinality);
            ((AbstractRange) range).setOrdered(ordered);
            property.setRange(range);
            assignPropertyType(field, property, range);
            assignInverse(property, range, inverseField);
        }
        property.setMandatory(mandatory);

        tasks.addAll(info.getTasks());
        property.setAnnotatedElement(field);
        property.setDeclaringClass(field.getDeclaringClass());
        property.setJavaType(field.getType());

        return new MetadataObjectInfo<>(property, tasks);
    }

    protected void assignInverse(MetaPropertyImpl property, Range range, String inverseField) {
        if (inverseField == null)
            return;

        if (!range.isClass())
            throw new IllegalArgumentException("Range of class type expected");

        MetaClass metaClass = range.asClass();
        MetaProperty inverseProp = metaClass.getProperty(inverseField);
        if (inverseProp == null)
            throw new RuntimeException(
                    String.format("Unable to assign inverse property '%s' for '%s'", inverseField, property));
        property.setInverse(inverseProp);
    }

    protected boolean isOrdered(Field field) {
        Class<?> type = field.getType();
        return List.class.isAssignableFrom(type) || LinkedHashSet.class.isAssignableFrom(type);
    }

    protected MetaClassImpl createClass(Class<?> clazz, String packageName, String className) {
        MetaClassImpl metaClass = (MetaClassImpl) session.getClass(clazz);
        if (metaClass != null) {
            return metaClass;
        } else if (packageName == null || clazz.getName().startsWith(packageName)) {
            metaClass = createMetaClass(packageName, className);
            metaClass.setJavaClass(clazz);

            Class<?> ancestor = clazz.getSuperclass();
            if (ancestor != null) {
                MetaClass ancestorClass = createClass(ancestor, packageName);
                if (ancestorClass != null) {
                    metaClass.addAncestor(ancestorClass);
                }
            }
            onClassLoaded(metaClass, clazz);

            return metaClass;
        } else {
            return null;
        }
    }

    protected class RangeInitTask implements MetadataObjectInitTask {

        private MetaProperty metaProperty;
        private Class rangeClass;
        private Map<String, Object> map;

        public RangeInitTask(MetaProperty metaProperty, Class rangeClass, Map<String, Object> map) {
            this.metaProperty = metaProperty;
            this.rangeClass = rangeClass;
            this.map = map;
        }

        @Override
        public String getWarning() {
            return String.format("Range for propery '%s' wasn't initialized (range class '%s')",
                    metaProperty.getName(), rangeClass.getName());
        }

        @Override
        public void execute() {
            MetaClass rangeClass = session.getClass(this.rangeClass);
            if (rangeClass == null) {
                throw new IllegalStateException(String.format("Can't find range class '%s' for property '%s.%s'",
                        this.rangeClass.getName(), metaProperty.getDomain(), metaProperty.getName()));
            } else {
                ClassRange range = new ClassRange(rangeClass);

                Range.Cardinality cardinality = (Range.Cardinality) map.get("cardinality");
                range.setCardinality(cardinality);
                if (Range.Cardinality.ONE_TO_MANY.equals(cardinality)
                        || Range.Cardinality.MANY_TO_MANY.equals(cardinality)) {
                    range.setOrdered((Boolean) map.get("ordered"));
                }

                Boolean mandatory = (Boolean) map.get("mandatory");
                if (mandatory != null) {
                    ((MetaPropertyImpl) metaProperty).setMandatory(mandatory);
                }

                ((MetaPropertyImpl) metaProperty).setRange(range);
                assignPropertyType(metaProperty.getAnnotatedElement(), metaProperty, range);

                assignInverse((MetaPropertyImpl) metaProperty, range, (String) map.get("inverseField"));
            }
        }
    }
}