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 if (toClass.isEnum()) { 248 valueConverter = new ValueConverter<>() { 249 @Override 250 @NonNull 251 public Optional<T> convert(@Nullable Object from) throws ValueConversionException { 252 if (from == null) 253 return Optional.empty(); 254 255 try { 256 return Optional.ofNullable((T) Enum.valueOf(toClass, from.toString())); 257 } catch (Exception e) { 258 throw new ValueConversionException(format("Unable to convert value '%s' of type %s to an instance of %s", 259 from, getFromType(), getToType()), e, getFromType(), from, getToType()); 260 } 261 } 262 263 @Override 264 @NonNull 265 public Type getFromType() { 266 return normalizedFromType; 267 } 268 269 @Override 270 @NonNull 271 public Type getToType() { 272 return normalizedToType; 273 } 274 275 @Override 276 @NonNull 277 public String toString() { 278 return format("%s{fromType=%s, toType=%s}", getClass().getSimpleName(), getFromType(), getToType()); 279 } 280 }; 281 282 this.enumConverterCache.putIfAbsent(new CacheKey(normalizedFromType, normalizedToType), valueConverter); 283 } 284 } 285 286 return Optional.ofNullable(valueConverter); 287 } 288 289 @NonNull 290 protected Type normalizePrimitiveTypeIfNecessary(@NonNull Type type) { 291 requireNonNull(type); 292 293 Type nonprimitiveEquivalent = PRIMITIVE_TYPES_TO_NONPRIMITIVE_EQUIVALENTS.get(type); 294 return nonprimitiveEquivalent == null ? type : nonprimitiveEquivalent; 295 } 296 297 @NonNull 298 protected Map<@NonNull CacheKey, @NonNull ValueConverter<?, ?>> getValueConvertersByCacheKey() { 299 return this.valueConvertersByCacheKey; 300 } 301 302 @NonNull 303 @Immutable 304 private static final class ReflexiveValueConverter<T> extends AbstractValueConverter<T, T> { 305 @NonNull 306 @Override 307 public Optional<T> performConversion(@Nullable T from) throws Exception { 308 return Optional.ofNullable(from); 309 } 310 } 311 312 @ThreadSafe 313 protected static final class CacheKey { 314 @NonNull 315 private final Type fromType; 316 @NonNull 317 private final Type toType; 318 319 public CacheKey(@NonNull Type fromType, 320 @NonNull Type toType) { 321 requireNonNull(fromType); 322 requireNonNull(toType); 323 324 this.fromType = fromType; 325 this.toType = toType; 326 } 327 328 @Override 329 public String toString() { 330 return format("%s{fromType=%s, toType=%s}", getClass().getSimpleName(), getFromType(), getToType()); 331 } 332 333 @Override 334 public boolean equals(@Nullable Object object) { 335 if (this == object) 336 return true; 337 if (!(object instanceof CacheKey cacheKey)) 338 return false; 339 340 return Objects.equals(getFromType(), cacheKey.getFromType()) && Objects.equals(getToType(), cacheKey.getToType()); 341 } 342 343 @Override 344 public int hashCode() { 345 return Objects.hash(getFromType(), getToType()); 346 } 347 348 @NonNull 349 public Type getFromType() { 350 return this.fromType; 351 } 352 353 @NonNull 354 public Type getToType() { 355 return this.toType; 356 } 357 } 358}