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 class ValueConverterRegistry { 050 @Nonnull 051 private static final ValueConverterRegistry SHARED_INSTANCE; 052 @Nonnull 053 private static final ValueConverter<?, ?> REFLEXIVE_VALUE_CONVERTER; 054 @Nonnull 055 private static final Map<Type, Type> PRIMITIVE_TYPES_TO_NONPRIMITIVE_EQUIVALENTS; 056 057 static { 058 REFLEXIVE_VALUE_CONVERTER = new ReflexiveValueConverter<>(); 059 SHARED_INSTANCE = new ValueConverterRegistry(); 060 061 // See https://docs.oracle.com/javase/tutorial/java/nutsandbolts/datatypes.html 062 PRIMITIVE_TYPES_TO_NONPRIMITIVE_EQUIVALENTS = Map.of( 063 int.class, Integer.class, 064 long.class, Long.class, 065 double.class, Double.class, 066 float.class, Float.class, 067 boolean.class, Boolean.class, 068 char.class, Character.class, 069 byte.class, Byte.class, 070 short.class, Short.class 071 ); 072 } 073 074 // This is explicitly typed as a ConcurrentHashMap because we may silently accumulate additional converters over time 075 // and this serves as a reminder that the Map instance must be threadsafe to accommodate. 076 // 077 // Use case: as new enum types are encountered, ValueConverter instances are generated and cached off. 078 // From a user's perspective, it would be burdensome to register converters for these ahead of time - 079 // it's preferable to have enum conversion "just work" for string names, which is almost always what's desired. 080 @Nonnull 081 private final ConcurrentHashMap<CacheKey, ValueConverter<?, ?>> valueConvertersByCacheKey; 082 083 /** 084 * The system's default shared registry instance. 085 * 086 * @return the shared registry instance 087 */ 088 @Nonnull 089 public static ValueConverterRegistry sharedInstance() { 090 return SHARED_INSTANCE; 091 } 092 093 /** 094 * Creates a registry with a sensible default set of converters as specified by {@link ValueConverters#defaultValueConverters()}. 095 */ 096 public ValueConverterRegistry() { 097 this(Set.of()); 098 } 099 100 /** 101 * Creates a registry with a sensible default set of converters as specified by {@link ValueConverters#defaultValueConverters()}, optionally supplemented with custom converters. 102 * 103 * @param customValueConverters the custom value converters to include in the registry 104 */ 105 public ValueConverterRegistry(@Nullable Set<ValueConverter<?, ?>> customValueConverters) { 106 if (customValueConverters == null) 107 customValueConverters = Set.of(); 108 109 Set<ValueConverter<?, ?>> defaultValueConverters = ValueConverters.defaultValueConverters(); 110 ConcurrentHashMap<CacheKey, ValueConverter<?, ?>> valueConvertersByCacheKey = new ConcurrentHashMap<>( 111 defaultValueConverters.size() 112 + customValueConverters.size() 113 + 1 // reflexive converter 114 + 100 // leave a little headroom for enum types that might accumulate over time 115 ); 116 117 // By default, we include out-of-the-box converters 118 for (ValueConverter<?, ?> defaultValueConverter : defaultValueConverters) 119 valueConvertersByCacheKey.put(extractCacheKeyFromValueConverter(defaultValueConverter), defaultValueConverter); 120 121 // We also include a "reflexive" converter which knows how to convert a type to itself 122 valueConvertersByCacheKey.put(extractCacheKeyFromValueConverter(REFLEXIVE_VALUE_CONVERTER), REFLEXIVE_VALUE_CONVERTER); 123 124 // Finally, register any additional converters that were provided 125 for (ValueConverter<?, ?> valueConverter : customValueConverters) 126 valueConvertersByCacheKey.put(extractCacheKeyFromValueConverter(valueConverter), valueConverter); 127 128 this.valueConvertersByCacheKey = valueConvertersByCacheKey; 129 } 130 131 /** 132 * Obtains a {@link ValueConverter} that matches the 'from' and 'to' type references specified. 133 * <p> 134 * Because of type erasure, you cannot directly express a generic type like <code>List<String>.class</code>. 135 * You must encode it as a type parameter - in this case, <code>new TypeReference<List<String>>() {}</code>. 136 * 137 * @param fromTypeReference reference to the 'from' type of the converter 138 * @param toTypeReference reference to the 'to' type of the converter 139 * @param <F> the 'from' type 140 * @param <T> the 'to' type 141 * @return a matching {@link ValueConverter}, or {@link Optional#empty()} if not found 142 */ 143 @Nonnull 144 public <F, T> Optional<ValueConverter<F, T>> get(@Nonnull TypeReference<F> fromTypeReference, 145 @Nonnull TypeReference<T> toTypeReference) { 146 requireNonNull(fromTypeReference); 147 requireNonNull(toTypeReference); 148 149 return getInternal(fromTypeReference.getType(), toTypeReference.getType()); 150 } 151 152 /** 153 * Obtain a {@link ValueConverter} that matches the 'from' and 'to' types specified. 154 * 155 * @param fromType the 'from' type 156 * @param toType the 'to' type 157 * @return a matching {@link ValueConverter}, or {@link Optional#empty()} if not found 158 */ 159 @Nonnull 160 public Optional<ValueConverter<Object, Object>> get(@Nonnull Type fromType, 161 @Nonnull Type toType) { 162 requireNonNull(fromType); 163 requireNonNull(toType); 164 165 return getInternal(fromType, toType); 166 } 167 168 @SuppressWarnings("unchecked") 169 @Nonnull 170 protected <F, T> Optional<ValueConverter<F, T>> getInternal(@Nonnull Type fromType, 171 @Nonnull Type toType) { 172 requireNonNull(fromType); 173 requireNonNull(toType); 174 175 Type normalizedFromType = normalizePrimitiveTypeIfNecessary(fromType); 176 Type normalizedToType = normalizePrimitiveTypeIfNecessary(toType); 177 178 // Reflexive case: from == to 179 if (normalizedFromType.equals(normalizedToType)) 180 return Optional.of((ValueConverter<F, T>) REFLEXIVE_VALUE_CONVERTER); 181 182 CacheKey cacheKey = new CacheKey(normalizedFromType, normalizedToType); 183 ValueConverter<F, T> valueConverter = (ValueConverter<F, T>) getValueConvertersByCacheKey().get(cacheKey); 184 185 // Special case for enums. 186 // If no converter was registered for converting a String to an Enum<?>, create a simple converter and cache it off 187 if (valueConverter == null && String.class.equals(normalizedFromType) && toType instanceof @SuppressWarnings("rawtypes")Class toClass) { 188 if (toClass.isEnum()) { 189 valueConverter = new ValueConverter<>() { 190 @Override 191 @Nonnull 192 public Optional<T> convert(@Nullable Object from) throws ValueConversionException { 193 if (from == null) 194 return null; 195 196 try { 197 return Optional.ofNullable((T) Enum.valueOf(toClass, from.toString())); 198 } catch (Exception e) { 199 throw new ValueConversionException(format("Unable to convert value '%s' of type %s to an instance of %s", 200 from, getFromType(), getToType()), e, getFromType(), from, getToType()); 201 } 202 } 203 204 @Override 205 @Nonnull 206 public Type getFromType() { 207 return normalizedFromType; 208 } 209 210 @Override 211 @Nonnull 212 public Type getToType() { 213 return normalizedToType; 214 } 215 216 @Override 217 @Nonnull 218 public String toString() { 219 return format("%s{fromType=%s, toType=%s}", getClass().getSimpleName(), getFromType(), getToType()); 220 } 221 }; 222 223 getValueConvertersByCacheKey().putIfAbsent(new CacheKey(normalizedFromType, normalizedToType), valueConverter); 224 } 225 } 226 227 return Optional.ofNullable(valueConverter); 228 } 229 230 @Nonnull 231 protected Type normalizePrimitiveTypeIfNecessary(@Nonnull Type type) { 232 requireNonNull(type); 233 234 Type nonprimitiveEquivalent = PRIMITIVE_TYPES_TO_NONPRIMITIVE_EQUIVALENTS.get(type); 235 return nonprimitiveEquivalent == null ? type : nonprimitiveEquivalent; 236 } 237 238 @Nonnull 239 protected CacheKey extractCacheKeyFromValueConverter(@Nonnull ValueConverter<?, ?> valueConverter) { 240 requireNonNull(valueConverter); 241 return new CacheKey(valueConverter.getFromType(), valueConverter.getToType()); 242 } 243 244 @Nonnull 245 protected Map<CacheKey, ValueConverter<?, ?>> getValueConvertersByCacheKey() { 246 return this.valueConvertersByCacheKey; 247 } 248 249 @Nonnull 250 @Immutable 251 private static final class ReflexiveValueConverter<T> extends AbstractValueConverter<T, T> { 252 @Nonnull 253 @Override 254 public Optional<T> performConversion(@Nullable T from) throws Exception { 255 return Optional.ofNullable(from); 256 } 257 } 258 259 @ThreadSafe 260 protected static final class CacheKey { 261 @Nonnull 262 private final Type fromType; 263 @Nonnull 264 private final Type toType; 265 266 public CacheKey(@Nonnull Type fromType, 267 @Nonnull Type toType) { 268 requireNonNull(fromType); 269 requireNonNull(toType); 270 271 this.fromType = fromType; 272 this.toType = toType; 273 } 274 275 @Override 276 public String toString() { 277 return format("%s{fromType=%s, toType=%s}", getClass().getSimpleName(), getFromType(), getToType()); 278 } 279 280 @Override 281 public boolean equals(@Nullable Object object) { 282 if (this == object) 283 return true; 284 if (!(object instanceof CacheKey cacheKey)) 285 return false; 286 287 return Objects.equals(getFromType(), cacheKey.getFromType()) && Objects.equals(getToType(), cacheKey.getToType()); 288 } 289 290 @Override 291 public int hashCode() { 292 return Objects.hash(getFromType(), getToType()); 293 } 294 295 @Nonnull 296 public Type getFromType() { 297 return this.fromType; 298 } 299 300 @Nonnull 301 public Type getToType() { 302 return this.toType; 303 } 304 } 305}