001/* 002 * Copyright 2022-2025 Revetware LLC. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016 017package com.soklet.converter; 018 019import javax.annotation.Nonnull; 020import javax.annotation.Nullable; 021import javax.annotation.concurrent.Immutable; 022import javax.annotation.concurrent.ThreadSafe; 023import java.lang.reflect.Type; 024import java.util.Map; 025import java.util.Objects; 026import java.util.Optional; 027import java.util.Set; 028import java.util.concurrent.ConcurrentHashMap; 029 030import static java.lang.String.format; 031import static java.util.Objects.requireNonNull; 032 033/** 034 * A collection of {@link ValueConverter} instances, supplemented with quality-of-life features that most applications need. 035 * <p> 036 * For example, the registry will automatically generate and cache off {@link ValueConverter} instances when a requested 'from' type is {@link String} 037 * and the 'to' type is an {@link Enum} if no converter was previously specified (this is almost always the behavior you want). 038 * <p> 039 * The registry will also perform primitive mapping when locating {@link ValueConverter} instances. 040 * For example, if a requested 'from' {@link String} and 'to' {@code int} are specified and that converter does not exist, but a 'from' {@link String} and 'to' {@link Integer} does exist, it will be returned. 041 * <p> 042 * Finally, reflexive {@link ValueConverter} instances are automatically created and cached off when the requested 'from' and 'to' types are identical. 043 * <p> 044 * Value conversion is documented in detail at <a href="https://www.soklet.com/docs/value-conversions">https://www.soklet.com/docs/value-conversions</a>. 045 * 046 * @author <a href="https://www.revetkn.com">Mark Allen</a> 047 */ 048@ThreadSafe 049public final class ValueConverterRegistry { 050 @Nonnull 051 private static final ValueConverter<?, ?> REFLEXIVE_VALUE_CONVERTER; 052 @Nonnull 053 private static final Map<Type, Type> PRIMITIVE_TYPES_TO_NONPRIMITIVE_EQUIVALENTS; 054 055 static { 056 REFLEXIVE_VALUE_CONVERTER = new ReflexiveValueConverter<>(); 057 058 // See https://docs.oracle.com/javase/tutorial/java/nutsandbolts/datatypes.html 059 PRIMITIVE_TYPES_TO_NONPRIMITIVE_EQUIVALENTS = Map.of( 060 int.class, Integer.class, 061 long.class, Long.class, 062 double.class, Double.class, 063 float.class, Float.class, 064 boolean.class, Boolean.class, 065 char.class, Character.class, 066 byte.class, Byte.class, 067 short.class, Short.class 068 ); 069 } 070 071 // This is explicitly typed as a ConcurrentHashMap because we may silently accumulate additional converters over time 072 // and this serves as a reminder that the Map instance must be threadsafe to accommodate. 073 // 074 // Use case: as new enum types are encountered, ValueConverter instances are generated and cached off. 075 // From a user's perspective, it would be burdensome to register converters for these ahead of time - 076 // it's preferable to have enum conversion "just work" for string names, which is almost always what's desired. 077 @Nonnull 078 private final ConcurrentHashMap<CacheKey, ValueConverter<?, ?>> valueConvertersByCacheKey; 079 080 /** 081 * Acquires a registry with a sensible default set of converters as specified by {@link ValueConverters#defaultValueConverters()}. 082 * <p> 083 * This method is guaranteed to return a new instance. 084 * 085 * @return a registry instance with sensible defaults 086 */ 087 @Nonnull 088 public static ValueConverterRegistry withDefaults() { 089 return withDefaultsSupplementedBy(Set.of()); 090 } 091 092 /** 093 * Acquires a registry with a sensible default set of converters as specified by {@link ValueConverters#defaultValueConverters()}, supplemented with custom converters. 094 * <p> 095 * This method is guaranteed to return a new instance. 096 * 097 * @param customValueConverters the custom value converters to include in the registry 098 * @return a registry instance with sensible defaults, supplemented with custom converters 099 */ 100 @Nonnull 101 public static ValueConverterRegistry withDefaultsSupplementedBy(@Nonnull Set<ValueConverter<?, ?>> customValueConverters) { 102 requireNonNull(customValueConverters); 103 104 Set<ValueConverter<?, ?>> defaultValueConverters = ValueConverters.defaultValueConverters(); 105 106 ConcurrentHashMap<CacheKey, ValueConverter<?, ?>> valueConvertersByCacheKey = new ConcurrentHashMap<>( 107 defaultValueConverters.size() 108 + customValueConverters.size() 109 + 1 // reflexive converter 110 + 100 // leave a little headroom for enum types that might accumulate over time 111 ); 112 113 // By default, we include out-of-the-box converters 114 for (ValueConverter<?, ?> defaultValueConverter : defaultValueConverters) 115 valueConvertersByCacheKey.put(extractCacheKeyFromValueConverter(defaultValueConverter), defaultValueConverter); 116 117 // We also include a "reflexive" converter which knows how to convert a type to itself 118 valueConvertersByCacheKey.put(extractCacheKeyFromValueConverter(REFLEXIVE_VALUE_CONVERTER), REFLEXIVE_VALUE_CONVERTER); 119 120 // Finally, register any additional converters that were provided 121 for (ValueConverter<?, ?> customValueConverter : customValueConverters) 122 valueConvertersByCacheKey.put(extractCacheKeyFromValueConverter(customValueConverter), customValueConverter); 123 124 return new ValueConverterRegistry(valueConvertersByCacheKey); 125 } 126 127 // TODO: we might add a factory method in the future that creates a totally-blank-slate registry that doesn't use defaults at all, doesn't create new ones for enums automatically, etc. 128 129 @Nonnull 130 private static CacheKey extractCacheKeyFromValueConverter(@Nonnull ValueConverter<?, ?> valueConverter) { 131 requireNonNull(valueConverter); 132 return new CacheKey(valueConverter.getFromType(), valueConverter.getToType()); 133 } 134 135 private ValueConverterRegistry(@Nonnull ConcurrentHashMap<CacheKey, ValueConverter<?, ?>> valueConvertersByCacheKey) { 136 requireNonNull(valueConvertersByCacheKey); 137 this.valueConvertersByCacheKey = valueConvertersByCacheKey; 138 } 139 140 /** 141 * Obtains a {@link ValueConverter} that matches the 'from' and 'to' type references specified. 142 * <p> 143 * Because of type erasure, you cannot directly express a generic type like <code>List<String>.class</code>. 144 * You must encode it as a type parameter - in this case, <code>new TypeReference<List<String>>() {}</code>. 145 * 146 * @param fromTypeReference reference to the 'from' type of the converter 147 * @param toTypeReference reference to the 'to' type of the converter 148 * @param <F> the 'from' type 149 * @param <T> the 'to' type 150 * @return a matching {@link ValueConverter}, or {@link Optional#empty()} if not found 151 */ 152 @Nonnull 153 public <F, T> Optional<ValueConverter<F, T>> get(@Nonnull TypeReference<F> fromTypeReference, 154 @Nonnull TypeReference<T> toTypeReference) { 155 requireNonNull(fromTypeReference); 156 requireNonNull(toTypeReference); 157 158 return getInternal(fromTypeReference.getType(), toTypeReference.getType()); 159 } 160 161 /** 162 * Obtain a {@link ValueConverter} that matches the 'from' and 'to' types specified. 163 * 164 * @param fromType the 'from' type 165 * @param toType the 'to' type 166 * @return a matching {@link ValueConverter}, or {@link Optional#empty()} if not found 167 */ 168 @Nonnull 169 public Optional<ValueConverter<Object, Object>> get(@Nonnull Type fromType, 170 @Nonnull Type toType) { 171 requireNonNull(fromType); 172 requireNonNull(toType); 173 174 return getInternal(fromType, toType); 175 } 176 177 @SuppressWarnings("unchecked") 178 @Nonnull 179 protected <F, T> Optional<ValueConverter<F, T>> getInternal(@Nonnull Type fromType, 180 @Nonnull Type toType) { 181 requireNonNull(fromType); 182 requireNonNull(toType); 183 184 Type normalizedFromType = normalizePrimitiveTypeIfNecessary(fromType); 185 Type normalizedToType = normalizePrimitiveTypeIfNecessary(toType); 186 187 // Reflexive case: from == to 188 if (normalizedFromType.equals(normalizedToType)) 189 return Optional.of((ValueConverter<F, T>) REFLEXIVE_VALUE_CONVERTER); 190 191 CacheKey cacheKey = new CacheKey(normalizedFromType, normalizedToType); 192 ValueConverter<F, T> valueConverter = (ValueConverter<F, T>) getValueConvertersByCacheKey().get(cacheKey); 193 194 // Special case for enums. 195 // If no converter was registered for converting a String to an Enum<?>, create a simple converter and cache it off 196 if (valueConverter == null && String.class.equals(normalizedFromType) && toType instanceof @SuppressWarnings("rawtypes")Class toClass) { 197 if (toClass.isEnum()) { 198 valueConverter = new ValueConverter<>() { 199 @Override 200 @Nonnull 201 public Optional<T> convert(@Nullable Object from) throws ValueConversionException { 202 if (from == null) 203 return Optional.empty(); 204 205 try { 206 return Optional.ofNullable((T) Enum.valueOf(toClass, from.toString())); 207 } catch (Exception e) { 208 throw new ValueConversionException(format("Unable to convert value '%s' of type %s to an instance of %s", 209 from, getFromType(), getToType()), e, getFromType(), from, getToType()); 210 } 211 } 212 213 @Override 214 @Nonnull 215 public Type getFromType() { 216 return normalizedFromType; 217 } 218 219 @Override 220 @Nonnull 221 public Type getToType() { 222 return normalizedToType; 223 } 224 225 @Override 226 @Nonnull 227 public String toString() { 228 return format("%s{fromType=%s, toType=%s}", getClass().getSimpleName(), getFromType(), getToType()); 229 } 230 }; 231 232 getValueConvertersByCacheKey().putIfAbsent(new CacheKey(normalizedFromType, normalizedToType), valueConverter); 233 } 234 } 235 236 return Optional.ofNullable(valueConverter); 237 } 238 239 @Nonnull 240 protected Type normalizePrimitiveTypeIfNecessary(@Nonnull Type type) { 241 requireNonNull(type); 242 243 Type nonprimitiveEquivalent = PRIMITIVE_TYPES_TO_NONPRIMITIVE_EQUIVALENTS.get(type); 244 return nonprimitiveEquivalent == null ? type : nonprimitiveEquivalent; 245 } 246 247 @Nonnull 248 protected Map<CacheKey, ValueConverter<?, ?>> getValueConvertersByCacheKey() { 249 return this.valueConvertersByCacheKey; 250 } 251 252 @Nonnull 253 @Immutable 254 private static final class ReflexiveValueConverter<T> extends AbstractValueConverter<T, T> { 255 @Nonnull 256 @Override 257 public Optional<T> performConversion(@Nullable T from) throws Exception { 258 return Optional.ofNullable(from); 259 } 260 } 261 262 @ThreadSafe 263 protected static final class CacheKey { 264 @Nonnull 265 private final Type fromType; 266 @Nonnull 267 private final Type toType; 268 269 public CacheKey(@Nonnull Type fromType, 270 @Nonnull Type toType) { 271 requireNonNull(fromType); 272 requireNonNull(toType); 273 274 this.fromType = fromType; 275 this.toType = toType; 276 } 277 278 @Override 279 public String toString() { 280 return format("%s{fromType=%s, toType=%s}", getClass().getSimpleName(), getFromType(), getToType()); 281 } 282 283 @Override 284 public boolean equals(@Nullable Object object) { 285 if (this == object) 286 return true; 287 if (!(object instanceof CacheKey cacheKey)) 288 return false; 289 290 return Objects.equals(getFromType(), cacheKey.getFromType()) && Objects.equals(getToType(), cacheKey.getToType()); 291 } 292 293 @Override 294 public int hashCode() { 295 return Objects.hash(getFromType(), getToType()); 296 } 297 298 @Nonnull 299 public Type getFromType() { 300 return this.fromType; 301 } 302 303 @Nonnull 304 public Type getToType() { 305 return this.toType; 306 } 307 } 308}