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

org.jtransfo.internal.ConverterHelper Maven / Gradle / Ivy

There is a newer version: 2.10
Show newest version
/*
 * This file is part of jTransfo, a library for converting to and from transfer objects.
 * Copyright (c) PROGS bvba, Belgium
 *
 * The program is available in open source according to the Apache License, Version 2.0.
 * For full licensing details, see LICENSE.txt in the project root.
 */

package org.jtransfo.internal;

import org.jtransfo.JTransfoException;
import org.jtransfo.MapOnlies;
import org.jtransfo.MapOnly;
import org.jtransfo.MappedBy;
import org.jtransfo.Named;
import org.jtransfo.NoConversionTypeConverter;
import org.jtransfo.NotMapped;
import org.jtransfo.PostConvert;
import org.jtransfo.PostConverter;
import org.jtransfo.PreConvert;
import org.jtransfo.PreConverter;
import org.jtransfo.ToConverter;
import org.jtransfo.TypeConverter;

import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * Helper class for building the converters for a pair of classes.
 */
public class ConverterHelper {

    private static final String DECLARED_TYPE_CONVERTER_CLASS = "Declared TypeConverter class ";

    private ReflectionHelper reflectionHelper = new ReflectionHelper();
    private ConcurrentHashMap typeConverterInstances = new ConcurrentHashMap<>();
    private List typeConvertersInOrder = Collections.emptyList(); // empty list for starters
    private ConcurrentHashMap preConverterInstances = new ConcurrentHashMap<>();
    private ConcurrentHashMap postConverterInstances = new ConcurrentHashMap<>();

    /**
     * Build the descriptor for conversion between given object types.
     *
     * @param toClass transfer object class, contains the annotations for the conversion
     * @param domainClass domain class as other side of conversion
     * @return conversion descriptor
     * @throws JTransfoException cannot build converter
     */
    public ToConverter getToConverter(Class toClass, Class domainClass) throws JTransfoException {
        ToConverter converter = withPreConverter(toClass);

        List domainFields = reflectionHelper.getSyntheticFields(domainClass);
        for (Field field : reflectionHelper.getFields(toClass)) {
            boolean isTransient = Modifier.isTransient(field.getModifiers());
            List notMapped = reflectionHelper.getAnnotationWithMeta(field, NotMapped.class);
            if (!isTransient && (0 == notMapped.size())) {
                List mappedBies = reflectionHelper.getAnnotationWithMeta(field, MappedBy.class);
                if (mappedBies.size() > 1) {
                    throw new JTransfoException("Field " + field.getName() + " on type " +
                            field.getDeclaringClass().getName() +
                            " MappedBy is ambiguous, check your meta-annotations.");
                }
                MappedBy mappedBy = null;
                if (1 == mappedBies.size()) {
                    mappedBy = mappedBies.get(0);
                }

                boolean isStatic = (0 != (field.getModifiers() & Modifier.STATIC));
                if (0 != mappedBies.size() || !isStatic) {
                    buildConverters(field, domainFields, domainClass, converter, mappedBy);
                }
            }
        }

        addPostConverter(converter, toClass);
        return converter;
    }

    private SyntheticField[] getDomainField(Field field, List domainFields, Class domainClass,
            String fieldParam, String pathParam, boolean readOnlyParam) {
        String domainFieldName = field.getName();
        String[] domainFieldPath = new String[0];
        boolean readOnlyField = (0 != (field.getModifiers() & Modifier.FINAL)) || readOnlyParam; // final -> read-only
        if (!MappedBy.DEFAULT_FIELD.equals(fieldParam)) {
            domainFieldName = fieldParam;
        }
        if (!MappedBy.DEFAULT_PATH.equals(pathParam)) {
            domainFieldPath = pathParam.split("\\.");
        }
        SyntheticField[] domainField;
        try {
            domainField = findField(domainFields, domainFieldName, domainFieldPath, domainClass,
                    readOnlyField);
        } catch (JTransfoException jte) {
            throw new JTransfoException(String.format("Cannot determine mapping for field %s in class " +
                            "%s. The field %s in class %s %scannot be found.",
                    field.getName(), field.getDeclaringClass().getName(),
                    domainFieldName, domainClass.getName(), withPath(domainFieldPath)), jte);
        }
        return domainField;
    }

    private SyntheticField[] getDomainField(Field field, List domainFields, Class domainClass,
            MappedBy mappedBy) {
        if (null == mappedBy) {
            return getDomainField(field, domainFields, domainClass,
                    MappedBy.DEFAULT_FIELD, MappedBy.DEFAULT_PATH, false);
        } else {
            return getDomainField(field, domainFields, domainClass,
                    mappedBy.field(), mappedBy.path(), mappedBy.readOnly());
        }
    }

    private void buildConverters(Field field, List domainFields, Class domainClass,
            ToConverter converter, MappedBy mappedBy) {
        SyntheticField sField = new SimpleSyntheticField(field);
        List mapOnlies = getMapOnlies(field);
        if (null == mapOnlies) {
            SyntheticField[] domainField = getDomainField(field, domainFields, domainClass, mappedBy);
            TypeConverter typeConverter = getDeclaredTypeConverter(mappedBy);
            if (null == typeConverter) {
                typeConverter = getDefaultTypeConverter(field.getGenericType(),
                        domainField[domainField.length - 1].getGenericType());
            }
            if (0 == (field.getModifiers() & Modifier.FINAL)) { // cannot write final fields
                converter.getToTo().add(new ToToConverter(sField, domainField, typeConverter));
            }
            if (null == mappedBy || !mappedBy.readOnly()) {
                converter.getToDomain().add(new ToDomainConverter(sField, domainField, typeConverter));
            }
        } else {
            TaggedConverter toTo = new TaggedConverter();
            TaggedConverter toDomain = new TaggedConverter();
            converter.getToTo().add(toTo);
            converter.getToDomain().add(toDomain);

            for (MapOnly mapOnly : mapOnlies) {
                // determine new domain field if path or field declare on mapOnly
                SyntheticField[] mapOnlyDomainField = null;
                if (!MappedBy.DEFAULT_PATH.equals(mapOnly.path()) || !MappedBy.DEFAULT_FIELD.equals(mapOnly.field())) {
                    mapOnlyDomainField = getDomainField(field, domainFields, domainClass,
                            mapOnly.field(), mapOnly.path(), mapOnly.readOnly());
                }
                if (null == mapOnlyDomainField) {
                    mapOnlyDomainField = getDomainField(field, domainFields, domainClass, mappedBy);
                }
                TypeConverter typeConverter = getDeclaredTypeConverter(mappedBy);
                if (null == typeConverter) {
                    typeConverter = getDefaultTypeConverter(field.getGenericType(),
                            mapOnlyDomainField[mapOnlyDomainField.length - 1].getGenericType());
                }
                TypeConverter moTypeConverter = getDeclaredTypeConverter(mapOnly, typeConverter);
                ToToConverter ttc = new ToToConverter(sField, mapOnlyDomainField, moTypeConverter);
                toTo.addConverters(ttc, mapOnly.value());
                if (!mapOnly.readOnly()) {
                    ToDomainConverter tdc = new ToDomainConverter(sField, mapOnlyDomainField, moTypeConverter);
                    toDomain.addConverters(tdc, mapOnly.value());
                }
            }
        }
    }

    private ToConverter withPreConverter(Class toClass) {
        List preConvertListAnnotations =
                reflectionHelper.getAnnotationWithMeta(toClass, PreConvert.List.class);
        List preConvertAnnotations = reflectionHelper.getAnnotationWithMeta(toClass, PreConvert.class);
        preConvertListAnnotations.forEach(list -> preConvertAnnotations.addAll(Arrays.asList(list.value())));
        if (preConvertAnnotations.isEmpty()) {
            return new ToConverter();
        } else {
            List preConverters = new ArrayList<>();
            for (PreConvert ann : preConvertAnnotations) {
                preConverters.add(
                        getConverter(ann.value(), ann.converterClass(), preConverterInstances, "preConverter"));
            }
            if (preConverters.size() == 1) {
                return new ToConverter(preConverters.get(0));
            } else {
                return new ToConverter(new CombinedPreConverter(preConverters));
            }
        }
    }

    private void addPostConverter(ToConverter converter, Class toClass) {
        List postConvertListAnnotations =
                reflectionHelper.getAnnotationWithMeta(toClass, PostConvert.List.class);
        List postConvertAnnotations = reflectionHelper.getAnnotationWithMeta(toClass, PostConvert.class);
        postConvertListAnnotations.forEach(list -> postConvertAnnotations.addAll(Arrays.asList(list.value())));
        for (PostConvert ann : postConvertAnnotations) {
            PostConverter postConverter =
                    getConverter(ann.value(), ann.converterClass(), postConverterInstances, "postConverter");
            converter.addToDomain(postConverter::postConvertToDomain);
            converter.addToTo(postConverter::postConvertToTo);
        }
    }

    private  C getConverter(String converterName, Class converterClass,
            Map converterInstances, String typeForException) {
        String name = converterName;
        if (PreConvert.DEFAULT_NAME.equals(name)) {
            name = converterClass.getName();
        }
        C converter = converterInstances.get(name);
        if (null == converter) {
            throw new JTransfoException(String.format("Cannot find %s %s.", typeForException, name));
        }
        return converter;
    }

    /**
     * Get the @MapOnly definitions which exist on a field, be it an individual {@link MapOnly} annotation or grouped
     * in {@link MapOnlies} or both.
     *
     * @param field field to get annotations for
     * @return list of annotations
     */
    List getMapOnlies(Field field) {
        List mapOnly = reflectionHelper.getAnnotationWithMeta(field, MapOnly.class);
        List mapOnlies = reflectionHelper.getAnnotationWithMeta(field, MapOnlies.class);
        if (0 == mapOnly.size() && 0 == mapOnlies.size()) {
            return null;
        }
        List res = new ArrayList<>();
        res.addAll(mapOnly);
        mapOnlies.forEach(mo -> Collections.addAll(res, mo.value()));
        return res;
    }

    /**
     * Convert path array to a readable representation.
     *
     * @param path array of path elements
     * @return original path string
     */
    String withPath(String[] path) {
        StringBuilder sb = new StringBuilder();
        if (path.length > 0) {
            sb.append(" (with path ");
            for (int i = 0; i < path.length - 1; i++) {
                sb.append(path[i]);
                sb.append(".");
            }
            sb.append(path[path.length - 1]);
            sb.append(") ");
        }
        return sb.toString();

    }

    /**
     * Find one field in a list of fields.
     *
     * @param domainFields list of fields in domain object
     * @param fieldName field to search
     * @param path list of intermediate fields for transitive fields
     * @param type type of object to find fields in
     * @param readOnlyField is the requested field read only
     * @return field with requested name or null when not found
     * @throws JTransfoException cannot find fields
     */
    SyntheticField[] findField(List domainFields, String fieldName, String[] path,
            Class type, boolean readOnlyField) throws JTransfoException {
        List fields = domainFields;
        SyntheticField[] result = new SyntheticField[path.length + 1];
        int index = 0;
        Class currentType = type;
        for (; index < path.length; index++) {
            boolean found = false;
            for (SyntheticField field : fields) {
                if (field.getName().equals(path[index])) {
                    found = true;
                    result[index] = field;
                    break;
                }
            }
            if (!found) {
                result[index] = new AccessorSyntheticField(reflectionHelper, currentType, path[index], readOnlyField);
            }
            currentType = result[index].getType();
            fields = reflectionHelper.getSyntheticFields(currentType);
        }
        for (SyntheticField field : fields) {
            if (field.getName().equals(fieldName)) {
                result[index] = field;
                return result;
            }
        }
        result[index] = new AccessorSyntheticField(reflectionHelper, currentType, fieldName, readOnlyField);
        return result;
    }

    /**
     * Get the {@link TypeConverter} which was specified in the {@link MappedBy} annotation (if any).
     *
     * @param typeConverterClassName1 highest priority type converter name (from class in annotation)
     * @param typeConverterClassName2  second highest priority type converter name (FQN)
     * @return type converter with highest precedence
     */
    TypeConverter getDeclaredTypeConverter(String typeConverterClassName1, String typeConverterClassName2) {
        String typeConverterClass = typeConverterClassName1;
        if (MappedBy.DefaultTypeConverter.class.getName().equals(typeConverterClass)) {
            typeConverterClass = typeConverterClassName2;
        }
        if (!MappedBy.DEFAULT_TYPE_CONVERTER.equals(typeConverterClass)) {
            TypeConverter typeConverter = typeConverterInstances.get(typeConverterClass);
            if (null == typeConverter) {
                try {
                    typeConverter = reflectionHelper.newInstance(typeConverterClass);
                    typeConverterInstances.put(typeConverterClass, typeConverter);
                } catch (ClassNotFoundException cnfe) {
                    throw new JTransfoException(DECLARED_TYPE_CONVERTER_CLASS + typeConverterClass +
                            " cannot be found.", cnfe);
                } catch (InstantiationException ie) {
                    throw new JTransfoException(DECLARED_TYPE_CONVERTER_CLASS + typeConverterClass +
                            " cannot be instantiated.", ie);
                } catch (IllegalAccessException iae) {
                    throw new JTransfoException(DECLARED_TYPE_CONVERTER_CLASS + typeConverterClass +
                            " cannot be accessed.", iae);
                } catch (ClassCastException cce) {
                    throw new JTransfoException(DECLARED_TYPE_CONVERTER_CLASS + typeConverterClass +
                            " cannot be cast to a TypeConverter.", cce);
                }
            }
            return typeConverter;
        }
        return null;
    }

    /**
     * Get declared type converter for a @MappedBy annotation.
     *
     * @param mappedBy MappedBy annotation
     * @return type converter with highest precedence
     */
    TypeConverter getDeclaredTypeConverter(MappedBy mappedBy) {
        if (null == mappedBy) {
            return null;
        }
        return getDeclaredTypeConverter(mappedBy.typeConverterClass().getName(), mappedBy.typeConverter());
    }

    /**
     * Determine type converter for a @MapOnly annotation (falling back to @MappedBy type converter.
     *
     * @param mapOnly MapOnly annotation
     * @param fallback type converter from MappedBy annotation
     * @return type converter with highest precedence
     */
     TypeConverter getDeclaredTypeConverter(MapOnly mapOnly, TypeConverter fallback) {
        if (null != mapOnly) {
            TypeConverter typeConverter = getDeclaredTypeConverter(
                    mapOnly.typeConverterClass().getName(), mapOnly.typeConverter());
            if (null != typeConverter) {
                return typeConverter;
            }
        }
        return fallback;
    }

    /**
     * Set the list of type converters. When searching a type conversion, the list is traversed front to back.
     *
     * @param typeConverters ordered list of type converters, the first converter in the list which can do the
     *      conversion is used.
     */
    void setTypeConvertersInOrder(Collection typeConverters) {
        LockableList newList = new LockableList<>();
        newList.addAll(typeConverters);
        newList.lock();
        typeConvertersInOrder = newList;

        // update list of converters to allow mentioning type converter by name, class name is used if no name provided
        for (TypeConverter tc : newList) {
            String name = null;
            if (tc instanceof Named) {
                name = ((Named) tc).getName();
            }
            if (null == name) {
                name = tc.getClass().getName();
            }
            typeConverterInstances.put(name, tc);
        }
    }

    /**
     * Get the default type converter given the field types to convert between.
     *
     * @param toField transfer object field class
     * @param domainField domain object field class
     * @return type converter
     */
    TypeConverter getDefaultTypeConverter(Type toField, Type domainField) {
        for (TypeConverter typeConverter : typeConvertersInOrder) {
            if (typeConverter.canConvert(toField, domainField)) {
                return typeConverter;
            }
        }
        return new NoConversionTypeConverter(); // default to no type conversion
    }

    /**
     * Set the list of type converters. When searching a type conversion, the list is traversed front to back.
     *
     * @param preConverters ordered list of type converters, the first converter in the list which can do the
     *      conversion is used.
     */
    void setPreConverters(Collection preConverters) {
        for (PreConverter pc : preConverters) {
            // register using specific name (if any)
            if (pc instanceof Named) {
                preConverterInstances.put(((Named) pc).getName(), pc);
            }
            // always register using class name
            preConverterInstances.put(pc.getClass().getName(), pc);
        }
    }

    /**
     * Set the list of type converters. When searching a type conversion, the list is traversed front to back.
     *
     * @param postConverters ordered list of type converters, the first converter in the list which can do the
     *      conversion is used.
     */
    void setPostConverters(Collection postConverters) {
        for (PostConverter pc : postConverters) {
            // register using specific name (if any)
            if (pc instanceof Named) {
                postConverterInstances.put(((Named) pc).getName(), pc);
            }
            // always register using class name
            postConverterInstances.put(pc.getClass().getName(), pc);
        }
    }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy