001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.juneau.commons.conversion; 018 019import static org.apache.juneau.commons.utils.Shorts.*; 020 021import java.lang.reflect.*; 022import java.util.*; 023import java.util.concurrent.*; 024import java.util.stream.*; 025 026/** 027 * An abstract {@link Converter} implementation that caches previously determined type conversions. 028 * 029 * <p> 030 * Conversion functions are discovered lazily on the first call for a given input/output type pair and 031 * stored in a two-level {@link ConcurrentHashMap} for subsequent fast lookup. 032 * 033 * <p> 034 * Subclasses implement {@link #findConversion(Class, Class)} to provide the actual conversion logic. 035 * The result is cached so that reflection or other expensive discovery happens only once per type pair. 036 * 037 * <h5 class='section'>Example:</h5> 038 * <p class='bjava'> 039 * <jk>public class</jk> MyConverter <jk>extends</jk> CachingConverter { 040 * 041 * <ja>@Override</ja> 042 * <jk>protected</jk> <I,O> Conversion<I,O> findConversion(Class<I> inType, Class<O> outType) { 043 * <jk>if</jk> (inType == String.<jk>class</jk> && outType == Integer.<jk>class</jk>) 044 * <jk>return</jk> (Conversion<I,O>) (Conversion<String,Integer>) (<jv>s</jv>, <jv>memberOf</jv>, <jv>session</jv>, <jv>args</jv>) -> Integer.<jsm>valueOf</jsm>(<jv>s</jv>); 045 * <jk>return null</jk>; 046 * } 047 * } 048 * </p> 049 * 050 * <h5 class='section'>Thread Safety:</h5> 051 * <p> 052 * This class is thread-safe. The cache uses {@link ConcurrentHashMap} for safe concurrent access. 053 * </p> 054 */ 055@SuppressWarnings({ 056 "unchecked" // Cast is safe: type parameter is verified at construction. 057}) 058public abstract class CachingConverter implements Converter { 059 060 // Sentinel stored in the cache when findConversion() returns null. 061 // ConcurrentHashMap does not permit null values, so we cannot store null directly. 062 // Using this sentinel avoids re-invoking findConversion() for unconvertable type pairs. 063 private static final Conversion<?,?> NO_CONVERSION = (in, memberOf, session, args) -> null; // HTT: sentinel body is never invoked; ConcurrentHashMap prohibits null values so we use this placeholder 064 065 /** 066 * Returns the appropriate default value for a null input to the given target type. 067 * 068 * <ul> 069 * <li>For {@link Optional}: returns {@link Optional#empty()}, or {@code Optional.of(Optional.empty())} for 070 * nested optional types such as {@code Optional<Optional<Integer>>}. 071 * <li>For primitives: returns the JVM zero/false default (e.g. {@code 0} for {@code int}). 072 * <li>For all other types: returns {@code null}. 073 * </ul> 074 * 075 * @param type The target type. 076 * @param args The generic type arguments (e.g. element type for {@link Optional}). 077 * @param <T> The target type. 078 * @return The appropriate null default. 079 */ 080 static <T> T nullDefault(Class<T> type, Class<?>... args) { 081 if (type == Optional.class) { 082 if (args.length > 0) 083 return (T) o(nullDefault(args[0], Arrays.copyOfRange(args, 1, args.length))); 084 return (T) oe(); 085 } 086 if (type == OptionalInt.class) 087 return (T) OptionalInt.empty(); 088 if (type == OptionalLong.class) 089 return (T) OptionalLong.empty(); 090 if (type == OptionalDouble.class) 091 return (T) OptionalDouble.empty(); 092 return type.isPrimitive() ? primitiveDefault(type) : null; 093 } 094 095 /** 096 * Returns the JVM default value for a primitive type, or <jk>null</jk> if the type is not primitive. 097 * 098 * <p> 099 * This is used to satisfy {@link #to} when the input is <jk>null</jk> and the target type is a 100 * non-boxed primitive (e.g. {@code int.class}), where returning <jk>null</jk> would be invalid. 101 * 102 * @param type The target type. 103 * @param <T> The target type. 104 * @return The JVM zero/false default, or <jk>null</jk> if {@code type} is not a primitive. 105 */ 106 static <T> T primitiveDefault(Class<T> type) { 107 if (type == Integer.TYPE) return (T) Integer.valueOf(0); 108 if (type == Long.TYPE) return (T) Long.valueOf(0L); 109 if (type == Double.TYPE) return (T) Double.valueOf(0.0d); 110 if (type == Float.TYPE) return (T) Float.valueOf(0.0f); 111 if (type == Boolean.TYPE) return (T) Boolean.FALSE; 112 if (type == Short.TYPE) return (T) Short.valueOf((short)0); 113 if (type == Byte.TYPE) return (T) Byte.valueOf((byte)0); 114 if (type == Character.TYPE) return (T) Character.valueOf('\0'); 115 return null; // HTT: no other primitive types exist in Java 116 } 117 118 // Two-level cache: input type -> output type -> conversion function (or NO_CONVERSION sentinel). 119 private final Map<Class<?>,Map<Class<?>,Conversion<?,?>>> conversions = new ConcurrentHashMap<>(); 120 121 /** 122 * Finds a conversion function from the specified input type to the specified output type. 123 * 124 * <p> 125 * This method is called lazily the first time a conversion between a given type pair is requested. 126 * The result is cached so subsequent calls for the same type pair bypass this method entirely. 127 * When this method returns <jk>null</jk>, a sentinel is stored so that unconvertable type pairs 128 * are also only evaluated once. 129 * 130 * <p> 131 * Implementations should return <jk>null</jk> if no conversion is possible for the given types, 132 * which will cause {@link #to(Object, Class)} and {@link #to(Object, Type, Type...)} to throw 133 * {@link InvalidConversionException} for that type pair. 134 * 135 * @param <I> The input type. 136 * @param <O> The output type. 137 * @param inType The runtime class of the input object. 138 * @param outType The target output class. 139 * @return A {@link Conversion} function, or <jk>null</jk> if no conversion is available. 140 */ 141 protected abstract <I,O> Conversion<I,O> findConversion(Class<I> inType, Class<O> outType); 142 143 private <I,O> Conversion<I,O> lookupConversion(Class<I> inType, Class<O> outType) { 144 var fn = conversions 145 .computeIfAbsent(inType, k -> new ConcurrentHashMap<>()) 146 .computeIfAbsent(outType, k -> { 147 Conversion<I,O> found = findConversion(inType, outType); 148 return found != null ? found : NO_CONVERSION; 149 }); 150 return fn == NO_CONVERSION ? null : (Conversion<I,O>) fn; 151 } 152 153 @Override 154 public boolean canConvert(Class<?> inType, Class<?> outType) { 155 if (inType == outType) 156 return true; 157 return lookupConversion(inType, outType) != null; 158 } 159 160 /** 161 * Converts the specified object to the specified type. 162 * 163 * <p> 164 * On the first call for a given input/output type pair, {@link #findConversion(Class, Class)} is 165 * invoked and the result is cached. Subsequent calls use the cached function directly. 166 * 167 * @param o The object to convert. Can be <jk>null</jk>. 168 * @param type The target type. 169 * @param <T> The target type. 170 * @return The converted object, or the primitive zero-value if the input is <jk>null</jk> and the target 171 * is a primitive type, or <jk>null</jk> otherwise. 172 * @throws InvalidConversionException If no conversion path exists from the input type to the target type. 173 */ 174 @Override 175 public <T> T to(Object o, Class<T> type) { 176 if (o == null) 177 return type.isPrimitive() ? primitiveDefault(type) : null; 178 var inType = o.getClass(); 179 if (inType == type) 180 return (T) o; 181 var fn = (Conversion<Object,T>) lookupConversion(inType, type); 182 if (fn == null) 183 throw new InvalidConversionException(inType, type); 184 return fn.to(o, null, (ConverterSession)null); 185 } 186 187 /** 188 * Converts the specified object to the specified parameterized type. 189 * 190 * <p> 191 * The raw class is extracted from {@code mainType} (supporting both {@link Class} and 192 * {@link ParameterizedType} values). Type arguments are extracted from {@code args} the same way 193 * and passed through to the cached {@link Conversion} function at call time. 194 * 195 * <p> 196 * The cache is keyed on the raw input and output classes only. The {@code args} are not part of 197 * the cache key — a single cached {@link Conversion} handles all parameterizations of a given 198 * output type. 199 * 200 * @param o The object to convert. Can be <jk>null</jk>. 201 * @param mainType The target type. May be a {@link Class} or {@link ParameterizedType}. 202 * @param args The type arguments of the target type (e.g. element type for collections). 203 * @param <T> The target type. 204 * @return The converted object, or <jk>null</jk> if the input is <jk>null</jk>. 205 * @throws InvalidConversionException If no conversion path exists from the input type to the target type. 206 */ 207 @Override 208 public <T> T to(Object o, Type mainType, Type... args) { 209 var rawType = (Class<T>) (mainType instanceof ParameterizedType mainType2 ? mainType2.getRawType() : (Class<?>) mainType); 210 var argClasses = Stream.of(args) 211 .map(t -> (Class<?>) (t instanceof ParameterizedType t2 ? t2.getRawType() : t)) 212 .toArray(Class[]::new); 213 if (o == null) 214 return nullDefault(rawType, argClasses); 215 var inType = o.getClass(); 216 var fn = (Conversion<Object,T>) lookupConversion(inType, rawType); 217 if (fn == null) 218 throw new InvalidConversionException(inType, rawType); 219 return fn.to(o, null, (ConverterSession)null, argClasses); 220 } 221 222 /** 223 * Converts the specified object to the specified type, using the given outer instance and converter session. 224 * 225 * <p> 226 * The session is forwarded to the cached {@link Conversion} function so conversion lambdas can access 227 * contextual objects such as {@link TimeZone} or {@link Locale}. 228 * 229 * @param o The object to convert. Can be <jk>null</jk>. 230 * @param memberOf The outer instance for non-static inner class construction, or <jk>null</jk>. 231 * @param session The converter session providing contextual objects, or <jk>null</jk>. 232 * @param type The target type. 233 * @param <T> The target type. 234 * @return The converted object, or the primitive zero-value if the input is <jk>null</jk> and the target 235 * is a primitive type, or <jk>null</jk> otherwise. 236 * @throws InvalidConversionException If no conversion path exists from the input type to the target type. 237 */ 238 @Override 239 public <T> T to(Object o, Object memberOf, ConverterSession session, Class<T> type) { 240 if (o == null) 241 return nullDefault(type); 242 var inType = o.getClass(); 243 if (inType == type) 244 return (T) o; 245 var fn = (Conversion<Object,T>) lookupConversion(inType, type); 246 if (fn == null) 247 throw new InvalidConversionException(inType, type); 248 return fn.to(o, memberOf, session); 249 } 250 251 /** 252 * Converts the specified object to the specified parameterized type, using the given outer instance and converter session. 253 * 254 * <p> 255 * The session is forwarded to the cached {@link Conversion} function so conversion lambdas can access 256 * contextual objects such as {@link TimeZone} or {@link Locale}. 257 * 258 * @param o The object to convert. Can be <jk>null</jk>. 259 * @param memberOf The outer instance for non-static inner class construction, or <jk>null</jk>. 260 * @param session The converter session providing contextual objects, or <jk>null</jk>. 261 * @param mainType The target type. May be a {@link Class} or {@link ParameterizedType}. 262 * @param args The type arguments of the target type (e.g. element type for collections). 263 * @param <T> The target type. 264 * @return The converted object, or <jk>null</jk> if the input is <jk>null</jk>. 265 * @throws InvalidConversionException If no conversion path exists from the input type to the target type. 266 */ 267 @Override 268 public <T> T to(Object o, Object memberOf, ConverterSession session, Type mainType, Type... args) { 269 var rawType = (Class<T>) (mainType instanceof ParameterizedType mainType2 ? mainType2.getRawType() : (Class<?>) mainType); 270 var argClasses = Stream.of(args) 271 .map(t -> (Class<?>) (t instanceof ParameterizedType t2 ? t2.getRawType() : t)) 272 .toArray(Class[]::new); 273 if (o == null) 274 return nullDefault(rawType, argClasses); 275 var inType = o.getClass(); 276 var fn = (Conversion<Object,T>) lookupConversion(inType, rawType); 277 if (fn == null) 278 throw new InvalidConversionException(inType, rawType); 279 var result = fn.to(o, memberOf, session, argClasses); 280 if (result == null && rawType.isPrimitive()) 281 return primitiveDefault(rawType); 282 return result; 283 } 284}