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}