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