richtercloud.reflection.form.builder.fieldhandler.MappingFieldHandler.java Source code

Java tutorial

Introduction

Here is the source code for richtercloud.reflection.form.builder.fieldhandler.MappingFieldHandler.java

Source

/**
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
    
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
    
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package richtercloud.reflection.form.builder.fieldhandler;

import java.awt.Component;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.SortedMap;
import java.util.TreeMap;
import javax.swing.JComponent;
import org.apache.commons.lang3.tuple.ImmutablePair;
import org.apache.commons.lang3.tuple.Pair;
import richtercloud.reflection.form.builder.AnyType;
import richtercloud.reflection.form.builder.ComponentHandler;
import richtercloud.reflection.form.builder.ReflectionFormBuilder;

/**
 * In order to KISS {@link TypeHandler} mappings are generated by {@link ClassMappingFactory}s.
 *
 * @author richter
 */
public class MappingFieldHandler<T, E extends FieldUpdateEvent<T>, R extends ReflectionFormBuilder, C extends Component>
        extends ResettableFieldHandler<T, E, R, C> {
    protected static <K, V> void validateMapping(List<Pair<K, V>> mapping, String argumentName) {
        if (argumentName == null) {
            throw new IllegalArgumentException("argumentName mustn't be null");
        }
        if (mapping == null) {
            throw new IllegalArgumentException(String.format("%s mustn't be null", argumentName));
        }
        if (mapping.contains(null)) {
            throw new IllegalArgumentException(String.format("%s mustn't contain null values", argumentName));
        }
    }

    private Map<Type, FieldHandler<?, ?, ?, ?>> classMapping = new HashMap<>();
    private Map<Class<?>, FieldHandler<?, ?, ?, ?>> primitiveMapping;

    /**
     * Creates a {@code ClassMappingFieldHandler}. It's recommended to generate
     * {@code classMapping} with {@link #generateClassMapping(java.lang.String, richtercloud.reflection.form.builder.components.AmountMoneyUsageStatisticsStorage, richtercloud.reflection.form.builder.components.AmountMoneyAdditionalCurrencyStorage, boolean) }, {@link #generateClassMappingDefault(java.lang.String) } or
     * {@link #generateClassMappingAmountMoneyFieldHandler(richtercloud.reflection.form.builder.components.AmountMoneyUsageStatisticsStorage, richtercloud.reflection.form.builder.components.AmountMoneyAdditionalCurrencyStorage, boolean) }.
     * @param classMapping
     * @param primitiveMapping
     */
    public MappingFieldHandler(Map<Type, FieldHandler<?, ?, ?, ?>> classMapping,
            Map<Class<?>, FieldHandler<?, ?, ?, ?>> primitiveMapping) {
        ReflectionFormBuilder.validateMapping(classMapping, "classMapping");
        ReflectionFormBuilder.validateMapping(primitiveMapping, "primitiveMapping");
        for (Class<?> primitiveMappingKey : primitiveMapping.keySet()) {
            if (!primitiveMappingKey.isPrimitive()) {
                throw new IllegalArgumentException("primitiveMapping only allows primitive classes as keys");
            }
        }
        this.classMapping.putAll(classMapping);
        this.primitiveMapping = primitiveMapping;
    }

    public Map<Type, FieldHandler<?, ?, ?, ?>> getClassMapping() {
        return Collections.unmodifiableMap(this.classMapping);
    }

    /**
     * Must never return {@code null}, otherwise {@link #handle(java.lang.reflect.Field, java.lang.Object, richtercloud.reflection.form.builder.fieldhandler.FieldUpdateListener, richtercloud.reflection.form.builder.ReflectionFormBuilder) } throws {@link IllegalArgumentException}.
     * @param field
     * @param instance
     * @param updateListener
     * @param reflectionFormBuilder
     * @return
     * @throws IllegalArgumentException
     * @throws IllegalAccessException
     * @throws FieldHandlingException
     * @throws InvocationTargetException
     * @throws NoSuchMethodException
     * @throws InstantiationException
     */
    protected Pair<JComponent, ComponentHandler<?>> handle0(final Field field, final Object instance,
            FieldUpdateListener<E> updateListener, R reflectionFormBuilder)
            throws IllegalArgumentException, IllegalAccessException, FieldHandlingException,
            InvocationTargetException, NoSuchMethodException, InstantiationException {
        JComponent retValue;
        T fieldValue = (T) field.get(instance);
        String fieldName = field.getName();
        Class<?> declaringClass = field.getDeclaringClass();

        FieldHandler fieldHandler = null;
        //can't have a generic type because it requires R to be passed down
        //the call hierarchy which requires to redesign the whole mapping
        //factory hierarchy which is extremely difficult due to entangled
        //generics like FieldHandler<T, FieldUpdateEvent<T>, ...>
        if (field.getType().isPrimitive()) {
            fieldHandler = primitiveMapping.get(field.getType());
        } else {
            // check exact type match
            fieldHandler = retrieveFieldHandler(field.getGenericType(), classMapping);
        }
        ComponentHandler<?> componentResettable;
        if (fieldHandler == null) {
            return null;
        }
        retValue = fieldHandler.handle(field, instance, new FieldUpdateListener<FieldUpdateEvent<?>>() {
            @Override
            public void onUpdate(FieldUpdateEvent<?> event) {
                try {
                    field.set(instance, event.getNewValue());
                } catch (IllegalArgumentException | IllegalAccessException ex) {
                    throw new RuntimeException(ex);
                }
            }
        }, reflectionFormBuilder);
        componentResettable = fieldHandler;
        return new ImmutablePair<JComponent, ComponentHandler<?>>(retValue, componentResettable);
    }

    /**
     *
     * @param fieldType always call with return value of {@link Field#getGenericType()
     * } in order to be the correct match
     * @param classMapping the class mapping to use (it might be necessary to
     * use different class mappings for entities and embeddables if components
     * check if field types are entities)
     * @return
     */
    public FieldHandler retrieveFieldHandler(Type fieldType, Map<Type, FieldHandler<?, ?, ?, ?>> classMapping) {
        Type classMappingKey = fieldType;
        if (fieldType instanceof ParameterizedType) {
            classMappingKey = retrieveClassMappingBestMatch((ParameterizedType) fieldType);
        }
        FieldHandler fieldHandler = classMapping.get(classMappingKey);
        // if exact type didn't match check closest match (only makes sense for
        // parameterized types as the others would have been found as direct
        // match already
        if (fieldHandler == null && fieldType instanceof ParameterizedType) {
            ParameterizedType fieldParameterizedType = (ParameterizedType) fieldType;
            Type candidate = retrieveClassMappingBestMatch(fieldParameterizedType);
            fieldHandler = classMapping.get(candidate);
        }
        return fieldHandler;
    }

    /**
     * Figures out candidates which the longest common prefix in the
     * {@code fieldParameterizedType} chain of (nested) generic types ignoring
     * specifications of {@link AnyType}. Then determines the candidates with
     * the smallest number of {@link AnyType} specifications in the chain. If
     * there're multiple with the same number of {@link AnyType} chooses the
     * first it finds which might lead to random choices.
     *
     * @param fieldParameterizedType the chain of generic types (remember to
     * retrieve this information with {@link Field#getGenericType() } instead of
     * {@link Field#getType() } from fields)
     * @return the choice result as described above or {@code null} if no
     * candidate exists
     */
    protected Type retrieveClassMappingBestMatch(ParameterizedType fieldParameterizedType) {
        //check in a row (walking a tree doesn't make sense because it's
        //agnostic of the position of the type
        SortedMap<Integer, List<ParameterizedType>> candidates = new TreeMap<>(); //TreeMap is a SortedMap
        for (Type mappingType : classMapping.keySet()) {
            if (!(mappingType instanceof ParameterizedType)) {
                continue;
            }
            ParameterizedType mappingParameterizedType = (ParameterizedType) mappingType;
            if (!mappingParameterizedType.getRawType().equals(fieldParameterizedType.getRawType())) {
                continue;
            }
            Type[] parameterizedTypeArguments = mappingParameterizedType.getActualTypeArguments();
            Type[] fieldParameterizedTypeArguments = fieldParameterizedType.getActualTypeArguments();
            for (int i = 0; i < Math.min(parameterizedTypeArguments.length,
                    fieldParameterizedTypeArguments.length); i++) {
                if (fieldParameterizedTypeArguments[i].equals(AnyType.class)) {
                    throw new IllegalArgumentException(String.format(
                            "type %s must only be used to declare placeholders in class mapping, not in classes (was used in field type %s",
                            AnyType.class, fieldParameterizedType));
                }
                // only compare raw type to raw type in the chain
                Type fieldParameterizedTypeArgument = fieldParameterizedTypeArguments[i];
                if (fieldParameterizedTypeArgument instanceof ParameterizedType) {
                    fieldParameterizedTypeArgument = ((ParameterizedType) fieldParameterizedTypeArgument)
                            .getRawType();
                }
                Type parameterizedTypeArgument = parameterizedTypeArguments[i];
                if (parameterizedTypeArgument instanceof ParameterizedType) {
                    parameterizedTypeArgument = ((ParameterizedType) parameterizedTypeArgument).getRawType();
                }
                //record AnyType matches as well
                boolean anyTypeMatch = AnyType.class.equals(parameterizedTypeArgument); //work around sucky debugger
                if (!parameterizedTypeArgument.equals(fieldParameterizedTypeArgument) && !anyTypeMatch) {
                    break;
                }
                int matchCount = i + 1;
                List<ParameterizedType> candidateList = candidates.get(matchCount);
                if (candidateList == null) {
                    candidateList = new LinkedList<>();
                    candidates.put(matchCount, candidateList);
                }
                candidateList.add(mappingParameterizedType);
            }
        }
        if (candidates.isEmpty()) {
            return null; //avoid NoSuchElementException
        }
        List<ParameterizedType> higestCandidatesList = candidates.get(candidates.lastKey());
        int lowestAnyCount = Integer.MAX_VALUE;
        ParameterizedType lowestAnyCountCandidate = null;
        for (ParameterizedType highestCandidateCandidate : higestCandidatesList) {
            int highestCandidateCandidateAnyCount = retrieveAnyCountRecursively(highestCandidateCandidate);
            if (highestCandidateCandidateAnyCount < lowestAnyCount) {
                lowestAnyCount = highestCandidateCandidateAnyCount;
                lowestAnyCountCandidate = highestCandidateCandidate;
            }
        }
        return lowestAnyCountCandidate;
    }

    protected int retrieveAnyCountRecursively(ParameterizedType type) {
        int retValue = 0;
        for (Type typeArgument : type.getActualTypeArguments()) {
            if (AnyType.class.equals(typeArgument)) {
                retValue += 1;
            }
            if (typeArgument instanceof ParameterizedType) {
                int recRetValue = retrieveAnyCountRecursively((ParameterizedType) typeArgument);
                retValue += recRetValue;
            }
        }
        return retValue;
    }
}