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