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