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.ThreadSafe;
023import java.lang.reflect.ParameterizedType;
024import java.lang.reflect.Type;
025import java.util.Arrays;
026import java.util.Collections;
027import java.util.List;
028import java.util.Optional;
029
030import static com.soklet.Utilities.trimAggressivelyToNull;
031import static java.lang.String.format;
032import static java.util.Objects.requireNonNull;
033
034/**
035 * Convenience superclass which provides default implementations of {@link ValueConverter} methods.
036 *
037 * @author <a href="https://www.revetkn.com">Mark Allen</a>
038 */
039@ThreadSafe
040public abstract class AbstractValueConverter<F, T> implements ValueConverter<F, T> {
041        @NonNull
042        private final Type fromType;
043        @NonNull
044        private final Type toType;
045
046        /**
047         * Supports subclasses that have both 'from' and 'to' generic types.
048         */
049        public AbstractValueConverter() {
050                List<Type> genericTypes = genericTypesForClass(getClass());
051
052                Type fromType = null;
053                Type toType = null;
054
055                if (genericTypes.size() == 2) {
056                        fromType = genericTypes.get(0);
057                        toType = genericTypes.get(1);
058                }
059
060                if (fromType == null || toType == null)
061                        throw new IllegalStateException(format("Unable to extract generic %s type information from %s",
062                                        ValueConverter.class.getSimpleName(), this));
063
064                this.fromType = fromType;
065                this.toType = toType;
066        }
067
068        /**
069         * Supports subclasses that have only a 'to' generic type, like {@link FromStringValueConverter}.
070         *
071         * @param fromType an explicitly-provided 'from' type
072         */
073        public AbstractValueConverter(@NonNull Type fromType) {
074                requireNonNull(fromType);
075
076                List<Type> genericTypes = genericTypesForClass(getClass());
077
078                Type toType = null;
079
080                if (genericTypes.size() == 1)
081                        toType = genericTypes.get(0);
082
083                if (toType == null)
084                        throw new IllegalStateException(format("Unable to extract generic %s type information from %s",
085                                        ValueConverter.class.getSimpleName(), this));
086
087                this.fromType = fromType;
088                this.toType = toType;
089        }
090
091        @NonNull
092        @Override
093        @SuppressWarnings("unchecked")
094        public final Optional<T> convert(@Nullable F from) throws ValueConversionException {
095                // Special handling for String types
096                if (from instanceof String && shouldTrimFromValues())
097                        from = (F) trimAggressivelyToNull((String) from);
098
099                try {
100                        return performConversion(from);
101                } catch (ValueConversionException e) {
102                        throw e;
103                } catch (Exception e) {
104                        throw new ValueConversionException(format("Unable to convert value '%s' of type %s to an instance of %s", from,
105                                        getFromType(), getToType()), e, getFromType(), from, getToType());
106                }
107        }
108
109        @NonNull
110        protected Boolean shouldTrimFromValues() {
111                return true;
112        }
113
114        /**
115         * Subclasses must implement this method to convert a 'from' instance to a 'to' instance.
116         *
117         * @param from the instance we are converting from
118         * @return an instance that was converted to
119         * @throws Exception if an error occured during conversion
120         */
121        @NonNull
122        public abstract Optional<T> performConversion(@Nullable F from) throws Exception;
123
124        @Override
125        @NonNull
126        public Type getFromType() {
127                return this.fromType;
128        }
129
130        @Override
131        @NonNull
132        public Type getToType() {
133                return this.toType;
134        }
135
136        @Override
137        @NonNull
138        public String toString() {
139                return format("%s{fromType=%s, toType=%s}", getClass().getSimpleName(), getFromType(), getToType());
140        }
141
142        @NonNull
143        static List<Type> genericTypesForClass(@Nullable Class<?> valueConverterClass) {
144                if (valueConverterClass == null)
145                        return List.of();
146
147                // TODO: this only works for simple cases (direct subclass or direct use of ValueConverter interface) and doesn't do full error handling.
148                List<Type> genericInterfaces = Arrays.asList(valueConverterClass.getGenericInterfaces());
149
150                // If not direct use of interface, try superclass (no error handling done yet)
151                if (genericInterfaces.size() == 0)
152                        genericInterfaces = Collections.singletonList(valueConverterClass.getGenericSuperclass());
153
154                // Figure out what the two type arguments are for ValueConverter
155                for (Type genericInterface : genericInterfaces) {
156                        if (genericInterface instanceof ParameterizedType) {
157                                Object rawType = ((ParameterizedType) genericInterface).getRawType();
158
159                                if (!ValueConverter.class.isAssignableFrom((Class<?>) rawType))
160                                        continue;
161
162                                Type[] genericTypes = ((ParameterizedType) genericInterface).getActualTypeArguments();
163                                return Arrays.asList(genericTypes);
164                        }
165                }
166
167                return List.of();
168        }
169}