Java tutorial
/** * 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; } }