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