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 class ValueConverterRegistry {
050        @Nonnull
051        private static final ValueConverterRegistry SHARED_INSTANCE;
052        @Nonnull
053        private static final ValueConverter<?, ?> REFLEXIVE_VALUE_CONVERTER;
054        @Nonnull
055        private static final Map<Type, Type> PRIMITIVE_TYPES_TO_NONPRIMITIVE_EQUIVALENTS;
056
057        static {
058                REFLEXIVE_VALUE_CONVERTER = new ReflexiveValueConverter<>();
059                SHARED_INSTANCE = new ValueConverterRegistry();
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<CacheKey, ValueConverter<?, ?>> valueConvertersByCacheKey;
082
083        /**
084         * The system's default shared registry instance.
085         *
086         * @return the shared registry instance
087         */
088        @Nonnull
089        public static ValueConverterRegistry sharedInstance() {
090                return SHARED_INSTANCE;
091        }
092
093        /**
094         * Creates a registry with a sensible default set of converters as specified by {@link ValueConverters#defaultValueConverters()}.
095         */
096        public ValueConverterRegistry() {
097                this(Set.of());
098        }
099
100        /**
101         * Creates a registry with a sensible default set of converters as specified by {@link ValueConverters#defaultValueConverters()}, optionally supplemented with custom converters.
102         *
103         * @param customValueConverters the custom value converters to include in the registry
104         */
105        public ValueConverterRegistry(@Nullable Set<ValueConverter<?, ?>> customValueConverters) {
106                if (customValueConverters == null)
107                        customValueConverters = Set.of();
108
109                Set<ValueConverter<?, ?>> defaultValueConverters = ValueConverters.defaultValueConverters();
110                ConcurrentHashMap<CacheKey, ValueConverter<?, ?>> valueConvertersByCacheKey = new ConcurrentHashMap<>(
111                                defaultValueConverters.size()
112                                                + customValueConverters.size()
113                                                + 1 // reflexive converter
114                                                + 100 // leave a little headroom for enum types that might accumulate over time
115                );
116
117                // By default, we include out-of-the-box converters
118                for (ValueConverter<?, ?> defaultValueConverter : defaultValueConverters)
119                        valueConvertersByCacheKey.put(extractCacheKeyFromValueConverter(defaultValueConverter), defaultValueConverter);
120
121                // We also include a "reflexive" converter which knows how to convert a type to itself
122                valueConvertersByCacheKey.put(extractCacheKeyFromValueConverter(REFLEXIVE_VALUE_CONVERTER), REFLEXIVE_VALUE_CONVERTER);
123
124                // Finally, register any additional converters that were provided
125                for (ValueConverter<?, ?> valueConverter : customValueConverters)
126                        valueConvertersByCacheKey.put(extractCacheKeyFromValueConverter(valueConverter), valueConverter);
127
128                this.valueConvertersByCacheKey = valueConvertersByCacheKey;
129        }
130
131        /**
132         * Obtains a {@link ValueConverter} that matches the 'from' and 'to' type references specified.
133         * <p>
134         * Because of type erasure, you cannot directly express a generic type like <code>List&lt;String&gt;.class</code>.
135         * You must encode it as a type parameter - in this case, <code>new TypeReference&lt;List&lt;String&gt;&gt;() &#123;&#125;</code>.
136         *
137         * @param fromTypeReference reference to the 'from' type of the converter
138         * @param toTypeReference   reference to the 'to' type of the converter
139         * @param <F>               the 'from' type
140         * @param <T>               the 'to' type
141         * @return a matching {@link ValueConverter}, or {@link Optional#empty()} if not found
142         */
143        @Nonnull
144        public <F, T> Optional<ValueConverter<F, T>> get(@Nonnull TypeReference<F> fromTypeReference,
145                                                                                                                                                                                                         @Nonnull TypeReference<T> toTypeReference) {
146                requireNonNull(fromTypeReference);
147                requireNonNull(toTypeReference);
148
149                return getInternal(fromTypeReference.getType(), toTypeReference.getType());
150        }
151
152        /**
153         * Obtain a {@link ValueConverter} that matches the 'from' and 'to' types specified.
154         *
155         * @param fromType the 'from' type
156         * @param toType   the 'to' type
157         * @return a matching {@link ValueConverter}, or {@link Optional#empty()} if not found
158         */
159        @Nonnull
160        public Optional<ValueConverter<Object, Object>> get(@Nonnull Type fromType,
161                                                                                                                                                                                                                        @Nonnull Type toType) {
162                requireNonNull(fromType);
163                requireNonNull(toType);
164
165                return getInternal(fromType, toType);
166        }
167
168        @SuppressWarnings("unchecked")
169        @Nonnull
170        protected <F, T> Optional<ValueConverter<F, T>> getInternal(@Nonnull Type fromType,
171                                                                                                                                                                                                                                                        @Nonnull Type toType) {
172                requireNonNull(fromType);
173                requireNonNull(toType);
174
175                Type normalizedFromType = normalizePrimitiveTypeIfNecessary(fromType);
176                Type normalizedToType = normalizePrimitiveTypeIfNecessary(toType);
177
178                // Reflexive case: from == to
179                if (normalizedFromType.equals(normalizedToType))
180                        return Optional.of((ValueConverter<F, T>) REFLEXIVE_VALUE_CONVERTER);
181
182                CacheKey cacheKey = new CacheKey(normalizedFromType, normalizedToType);
183                ValueConverter<F, T> valueConverter = (ValueConverter<F, T>) getValueConvertersByCacheKey().get(cacheKey);
184
185                // Special case for enums.
186                // If no converter was registered for converting a String to an Enum<?>, create a simple converter and cache it off
187                if (valueConverter == null && String.class.equals(normalizedFromType) && toType instanceof @SuppressWarnings("rawtypes")Class toClass) {
188                        if (toClass.isEnum()) {
189                                valueConverter = new ValueConverter<>() {
190                                        @Override
191                                        @Nonnull
192                                        public Optional<T> convert(@Nullable Object from) throws ValueConversionException {
193                                                if (from == null)
194                                                        return null;
195
196                                                try {
197                                                        return Optional.ofNullable((T) Enum.valueOf(toClass, from.toString()));
198                                                } catch (Exception e) {
199                                                        throw new ValueConversionException(format("Unable to convert value '%s' of type %s to an instance of %s",
200                                                                        from, getFromType(), getToType()), e, getFromType(), from, getToType());
201                                                }
202                                        }
203
204                                        @Override
205                                        @Nonnull
206                                        public Type getFromType() {
207                                                return normalizedFromType;
208                                        }
209
210                                        @Override
211                                        @Nonnull
212                                        public Type getToType() {
213                                                return normalizedToType;
214                                        }
215
216                                        @Override
217                                        @Nonnull
218                                        public String toString() {
219                                                return format("%s{fromType=%s, toType=%s}", getClass().getSimpleName(), getFromType(), getToType());
220                                        }
221                                };
222
223                                getValueConvertersByCacheKey().putIfAbsent(new CacheKey(normalizedFromType, normalizedToType), valueConverter);
224                        }
225                }
226
227                return Optional.ofNullable(valueConverter);
228        }
229
230        @Nonnull
231        protected Type normalizePrimitiveTypeIfNecessary(@Nonnull Type type) {
232                requireNonNull(type);
233
234                Type nonprimitiveEquivalent = PRIMITIVE_TYPES_TO_NONPRIMITIVE_EQUIVALENTS.get(type);
235                return nonprimitiveEquivalent == null ? type : nonprimitiveEquivalent;
236        }
237
238        @Nonnull
239        protected CacheKey extractCacheKeyFromValueConverter(@Nonnull ValueConverter<?, ?> valueConverter) {
240                requireNonNull(valueConverter);
241                return new CacheKey(valueConverter.getFromType(), valueConverter.getToType());
242        }
243
244        @Nonnull
245        protected Map<CacheKey, ValueConverter<?, ?>> getValueConvertersByCacheKey() {
246                return this.valueConvertersByCacheKey;
247        }
248
249        @Nonnull
250        @Immutable
251        private static final class ReflexiveValueConverter<T> extends AbstractValueConverter<T, T> {
252                @Nonnull
253                @Override
254                public Optional<T> performConversion(@Nullable T from) throws Exception {
255                        return Optional.ofNullable(from);
256                }
257        }
258
259        @ThreadSafe
260        protected static final class CacheKey {
261                @Nonnull
262                private final Type fromType;
263                @Nonnull
264                private final Type toType;
265
266                public CacheKey(@Nonnull Type fromType,
267                                                                                @Nonnull Type toType) {
268                        requireNonNull(fromType);
269                        requireNonNull(toType);
270
271                        this.fromType = fromType;
272                        this.toType = toType;
273                }
274
275                @Override
276                public String toString() {
277                        return format("%s{fromType=%s, toType=%s}", getClass().getSimpleName(), getFromType(), getToType());
278                }
279
280                @Override
281                public boolean equals(@Nullable Object object) {
282                        if (this == object)
283                                return true;
284                        if (!(object instanceof CacheKey cacheKey))
285                                return false;
286
287                        return Objects.equals(getFromType(), cacheKey.getFromType()) && Objects.equals(getToType(), cacheKey.getToType());
288                }
289
290                @Override
291                public int hashCode() {
292                        return Objects.hash(getFromType(), getToType());
293                }
294
295                @Nonnull
296                public Type getFromType() {
297                        return this.fromType;
298                }
299
300                @Nonnull
301                public Type getToType() {
302                        return this.toType;
303                }
304        }
305}