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&lt;String&gt;.class</code>.
190         * You must encode it as a type parameter - in this case, <code>new TypeReference&lt;List&lt;String&gt;&gt;() &#123;&#125;</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}