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

org.bbottema.javareflection.valueconverter.ValueConversionHelper Maven / Gradle / Ivy

package org.bbottema.javareflection.valueconverter;

import lombok.experimental.UtilityClass;
import org.bbottema.javareflection.TypeUtils;
import org.bbottema.javareflection.util.MiscUtil;
import org.bbottema.javareflection.util.graph.GraphHelper;
import org.bbottema.javareflection.util.graph.Node;
import org.bbottema.javareflection.valueconverter.converters.BooleanConverters;
import org.bbottema.javareflection.valueconverter.converters.CharacterConverters;
import org.bbottema.javareflection.valueconverter.converters.NumberConverters;
import org.bbottema.javareflection.valueconverter.converters.StringConverters;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import static java.util.Arrays.asList;
import static org.bbottema.javareflection.util.MiscUtil.trustedCast;

/**
 * This reflection utility class predicts (and converts) which types a specified value can be converted into. It can only do conversions of
 * known 'common' types, which include:
 * 
    *
  • Any {@link Number} type (Integer, Character, Double, byte, etc.)
  • *
  • String
  • *
  • Boolean
  • *
  • Character
  • *
* In addition enums can be converted as well. * * @see IncompatibleTypeException */ @UtilityClass public final class ValueConversionHelper { /** * A list of all primitive number types. */ private static final List> PRIMITIVE_NUMBER_TYPES = asList(new Class[] { byte.class, short.class, int.class, long.class, float.class, double.class }); /** * Contains all user-provided converters. User converters also act as intermediate converters, ie. if a user converter can go to int, * double is automatically supported as well as common conversion. */ private static final Map, Map, ValueFunction>> valueConverters = new HashMap<>(); /** * Graph of from-to type conversions so we can calculate shortes conversion path between two types. */ private static final Map, Node>> converterGraph = new HashMap<>(); private static final int LOW_CONVERTER_PRIORITY = 10; // higher edge weight, heavier in cost private static final int HIGH_CONVERTER_PRIORITY = 1; // lower edge weight, lighter in cost static { resetDefaultConverters(); } public static void resetDefaultConverters() { valueConverters.clear(); final Collection> defaultConverters = new HashSet<>(); defaultConverters.addAll(NumberConverters.NUMBER_CONVERTERS); defaultConverters.addAll(BooleanConverters.BOOLEAN_CONVERTERS); defaultConverters.addAll(CharacterConverters.CHARACTER_CONVERTERS); defaultConverters.addAll(StringConverters.STRING_CONVERTERS); for (ValueFunction numberConverter : defaultConverters) { registerValueConverter(numberConverter); } } /** * Registers a user-provided converter. User converters also act as intermediate converters, ie. if a user converter can go to int, * double is automatically supported as well as common conversion. */ @SuppressWarnings({"unused", "WeakerAccess"}) public static void registerValueConverter(final ValueFunction userConverter) { if (!valueConverters.containsKey(userConverter.getFromType())) { valueConverters.put(userConverter.getFromType(), new HashMap, ValueFunction>()); } valueConverters.get(userConverter.getFromType()).put(userConverter.getTargetType(), MiscUtil.>trustedCast(userConverter)); updateTypeGraph(); TypeUtils.clearCaches(); } private static void updateTypeGraph() { converterGraph.clear(); // add nodes and edges for (Map.Entry, Map, ValueFunction>> convertersForFromType : valueConverters.entrySet()) { Class fromType = convertersForFromType.getKey(); Node> fromNode = converterGraph.containsKey(fromType) ? converterGraph.get(fromType) : new Node>(fromType); converterGraph.put(fromType, fromNode); for (Class toType : convertersForFromType.getValue().keySet()) { Node> toNode = converterGraph.containsKey(toType) ? converterGraph.get(toType) : new Node>(toType); converterGraph.put(toType, toNode); ValueFunction converter = convertersForFromType.getValue().get(toType); fromNode.getToNodes().put(toNode, determineConversionCost(converter)); // edge } } } private static Integer determineConversionCost(ValueFunction converter) { if (converter.getFromType() == converter.getTargetType()) { return 0; } else { String converterPackage = ValueConversionHelper.class.getPackage().toString(); boolean isSystemConverter = converter.getClass().getPackage().toString().contains(converterPackage); return isSystemConverter ? LOW_CONVERTER_PRIORITY : HIGH_CONVERTER_PRIORITY; } } @SuppressWarnings("WeakerAccess") public static boolean isCommonType(final Class c) { Map, ValueFunction> classValueFunctionMap = valueConverters.get(c); return valueConverters.containsKey(c) && (classValueFunctionMap.keySet().size() > 1 || !classValueFunctionMap.keySet().contains(String.class)); } /** * Determines to which types the specified value (its type) can be converted to. Most common types can be converted to most other common * types and all types can be converted into a String using {@link Object#toString()}. * * @param fromType The input type to find compatible conversion output types for * @return The list with compatible conversion output types. */ @SuppressWarnings("WeakerAccess") @NotNull public static Set> collectRegisteredCompatibleTargetTypes(final Class fromType) { Set> compatibleTypes = new HashSet<>(Collections.>singleton(fromType)); if (converterGraph.containsKey(fromType)) { for (Node> reachableNode : GraphHelper.findReachableNodes(converterGraph.get(fromType))) { compatibleTypes.add(reachableNode.getType()); } } return compatibleTypes; } /** * @return Whether targetType can be derived from fromType. */ @SuppressWarnings("unused") public static boolean typesCompatible(final Class fromType, final Class targetType) { if (targetType.isAssignableFrom(fromType)) { return true; } else { for (Class registeredCompatibleTargetType : collectCompatibleTargetTypes(fromType)) { if (targetType.isAssignableFrom(registeredCompatibleTargetType)) { return true; } } } return false; } public static Set> collectCompatibleTargetTypes(Class fromType) { checkForAndRegisterToStringConverter(fromType); Set> compatibleTargetTypes = new HashSet<>(); Node> fromNode = converterGraph.get(fromType); for (Map, ValueFunction> convertersForFromTypes : valueConverters.values()) { for (Class targetType : convertersForFromTypes.keySet()) { if (isCompatibleTargetType(fromNode, targetType)) { compatibleTargetTypes.add(targetType); } } } return compatibleTargetTypes; } private static boolean isCompatibleTargetType(Node> fromNode, Class targetType) { for (Node> toNode : collectTypeCompatibleNodes(targetType)) { if (GraphHelper.isPathPossible(fromNode, toNode)) { return true; } } return false; } /** * Converts a list of values to their converted form, as indicated by the specified targetTypes. * * @param args The list with value to convert. * @param targetTypes The output types the specified values should be converted into. * @param useOriginalValueWhenIncompatible Indicates whether an exception should be thrown for inconvertible values or that the original * value should be used instead. Basically change mode to "convert what you can". * @return Array containing converted values where convertible or the original value otherwise. * @throws IncompatibleTypeException Thrown when unable to convert and not use the original value. */ @NotNull public static Object[] convert(final Object[] args, final Class[] targetTypes, boolean useOriginalValueWhenIncompatible) throws IncompatibleTypeException { if (args.length != targetTypes.length) { throw new IllegalStateException("number of target types should match the number of arguments"); } final Object[] convertedValues = new Object[args.length]; for (int i = 0; i < targetTypes.length; i++) { try { convertedValues[i] = convert(args[i], targetTypes[i]); } catch (IncompatibleTypeException e) { if (useOriginalValueWhenIncompatible) { // simply take over the original value and keep converting where possible convertedValues[i] = args[i]; } else { throw e; } } } return convertedValues; } /** * Converts a single value into a target output datatype. Only input/output pairs should be passed in here according to the possible * conversions as determined by {@link #collectRegisteredCompatibleTargetTypes(Class)}.
*
* First checks if the input and output types aren't the same. Then the conversions are checked for and done in the following order: *
    *
  1. conversion to String (value.toString())
  2. *
* * @param fromValue The value to convert. * @param targetType The target data type the value should be converted into. * @return The converted value according the specified target data type. * @throws IncompatibleTypeException Thrown by the various convert() methods used. */ @SuppressWarnings("unchecked") @Nullable public static T convert(@Nullable final Object fromValue, final Class targetType) throws IncompatibleTypeException { if (fromValue == null) { return null; } else if (targetType.isAssignableFrom(fromValue.getClass())) { return trustedCast(fromValue); } else { checkForAndRegisterEnumConverter(targetType); checkForAndRegisterToStringConverter(fromValue.getClass()); return convertWithConversionGraph(fromValue, targetType); } } @SuppressWarnings("unchecked") @NotNull private static T convertWithConversionGraph(@NotNull Object fromValue, @NotNull Class targetType) { final Node> fromNode = converterGraph.get(fromValue.getClass()); if (fromNode != null) { for (Node> toNode : collectTypeCompatibleNodes(targetType)) { for (List>> conversionPathAscending : GraphHelper.findAllPathsAscending(fromNode, toNode)) { try { Object evolvingValueToConvert = fromValue; for (Node> nodeInConversionPath : conversionPathAscending) { Class currentFromType = evolvingValueToConvert.getClass(); Class currentToType = nodeInConversionPath.getType(); evolvingValueToConvert = valueConverters.get(currentFromType).get(currentToType).convertValue(evolvingValueToConvert); } return (T) evolvingValueToConvert; } catch (IncompatibleTypeException e) { // keep trying conversion paths... } } } } // conversion paths exhausted. throw new IncompatibleTypeException(fromValue, fromValue.getClass(), targetType); } @SuppressWarnings("unchecked") private static > void checkForAndRegisterEnumConverter(Class targetType) { if (Enum.class.isAssignableFrom(targetType)) { if (!valueConverters.get(String.class).containsKey(targetType)) { registerValueConverter(StringConverters.produceStringToEnumConverter((Class) targetType)); } } } private static void checkForAndRegisterToStringConverter(Class fromType) { if (!valueConverters.containsKey(fromType) || !valueConverters.get(fromType).containsKey(String.class)) { registerValueConverter(StringConverters.produceTypeToStringConverter(fromType)); } } static Set>> collectTypeCompatibleNodes(Class targetType) { final Set>> typeCompatibleNodes = new HashSet<>(); for (Map.Entry, Node>> converterNodeEntry : converterGraph.entrySet()) { if (targetType.isAssignableFrom(converterNodeEntry.getKey())) { typeCompatibleNodes.add(converterNodeEntry.getValue()); } } return typeCompatibleNodes; } /** * Returns whether a {@link Class} is a primitive number. * * @param targetType The class to check whether it's a number. * @return Whether specified class is a primitive number. */ @SuppressWarnings("WeakerAccess") public static boolean isPrimitiveNumber(final Class targetType) { return PRIMITIVE_NUMBER_TYPES.contains(targetType); } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy