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