001// *************************************************************************************************************************** 002// * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file * 003// * distributed with this work for additional information regarding copyright ownership. The ASF licenses this file * 004// * to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance * 005// * with the License. You may obtain a copy of the License at * 006// * * 007// * http://www.apache.org/licenses/LICENSE-2.0 * 008// * * 009// * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an * 010// * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the * 011// * specific language governing permissions and limitations under the License. * 012// *************************************************************************************************************************** 013package org.apache.juneau.reflect; 014 015import java.util.concurrent.*; 016 017import static org.apache.juneau.common.internal.StringUtils.*; 018import static org.apache.juneau.common.internal.ThrowableUtils.*; 019 020import java.lang.reflect.*; 021import java.util.*; 022 023/** 024 * Cache of object that convert POJOs to and from common types such as strings, readers, and input streams. 025 * 026 * <h5 class='section'>See Also:</h5><ul> 027 * </ul> 028 */ 029public class Mutaters { 030 private static final ConcurrentHashMap<Class<?>,Map<Class<?>,Mutater<?,?>>> CACHE = new ConcurrentHashMap<>(); 031 032 /** 033 * Represents a non-existent transform. 034 */ 035 public static final Mutater<Object,Object> NULL = new Mutater<>() { 036 @Override 037 public Object mutate(Object outer, Object in) { 038 return null; 039 } 040 }; 041 042 // Special cases. 043 static { 044 045 // TimeZone doesn't follow any standard conventions. 046 add(String.class, TimeZone.class, 047 new Mutater<String,TimeZone>() { 048 @Override public TimeZone mutate(Object outer, String in) { 049 return TimeZone.getTimeZone(in); 050 } 051 } 052 ); 053 add(TimeZone.class, String.class, 054 new Mutater<TimeZone,String>() { 055 @Override public String mutate(Object outer, TimeZone in) { 056 return in.getID(); 057 } 058 } 059 ); 060 061 // Locale(String) doesn't work on strings like "ja_JP". 062 add(String.class, Locale.class, 063 new Mutater<String,Locale>() { 064 @Override 065 public Locale mutate(Object outer, String in) { 066 return Locale.forLanguageTag(in.replace('_', '-')); 067 } 068 } 069 ); 070 071 // String-to-Boolean transform should allow for "null" keyword. 072 add(String.class, Boolean.class, 073 new Mutater<String,Boolean>() { 074 @Override 075 public Boolean mutate(Object outer, String in) { 076 if (in == null || "null".equals(in) || in.isEmpty()) 077 return null; 078 return Boolean.valueOf(in); 079 } 080 } 081 ); 082 } 083 084 /** 085 * Adds a transform for the specified input/output types. 086 * 087 * @param ic The input type. 088 * @param oc The output type. 089 * @param t The transform for converting the input to the output. 090 */ 091 public static synchronized void add(Class<?> ic, Class<?> oc, Mutater<?,?> t) { 092 Map<Class<?>,Mutater<?,?>> m = CACHE.get(oc); 093 if (m == null) { 094 m = new ConcurrentHashMap<>(); 095 CACHE.put(oc, m); 096 } 097 m.put(ic, t); 098 } 099 100 /** 101 * Returns the transform for converting the specified input type to the specified output type. 102 * 103 * @param <I> The input type. 104 * @param <O> The output type. 105 * @param ic The input type. 106 * @param oc The output type. 107 * @return The transform for performing the conversion, or <jk>null</jk> if the conversion cannot be made. 108 */ 109 @SuppressWarnings({ "unchecked", "rawtypes" }) 110 public static <I,O> Mutater<I,O> get(Class<I> ic, Class<O> oc) { 111 112 if (ic == null || oc == null) 113 return null; 114 115 Map<Class<?>,Mutater<?,?>> m = CACHE.get(oc); 116 if (m == null) { 117 m = new ConcurrentHashMap<>(); 118 CACHE.putIfAbsent(oc, m); 119 m = CACHE.get(oc); 120 } 121 122 Mutater t = m.get(ic); 123 124 if (t == null) { 125 t = find(ic, oc, m); 126 m.put(ic, t); 127 } 128 129 return t == NULL ? null : t; 130 } 131 132 /** 133 * Returns the transform for converting the specified input type to the specified output type. 134 * 135 * @param <I> The input type. 136 * @param <O> The output type. 137 * @param ic The input type. 138 * @param oc The output type. 139 * @return The transform for performing the conversion, or <jk>null</jk> if the conversion cannot be made. 140 */ 141 public static <I,O> boolean hasMutate(Class<I> ic, Class<O> oc) { 142 return get(ic, oc) != NULL; 143 } 144 145 @SuppressWarnings({"unchecked","rawtypes"}) 146 private static Mutater find(Class<?> ic, Class<?> oc, Map<Class<?>,Mutater<?,?>> m) { 147 148 if (ic == oc) { 149 return new Mutater() { 150 @Override public Object mutate(Object outer, Object in) { 151 return in; 152 } 153 }; 154 } 155 156 ClassInfo ici = ClassInfo.of(ic), oci = ClassInfo.of(oc); 157 158 ClassInfo pic = ici.getAnyParent(x -> m.get(x.inner()) != null); 159 if (pic != null) 160 return m.get(pic.inner()); 161 162 if (ic == String.class) { 163 Class<?> oc2 = oci.hasPrimitiveWrapper() ? oci.getPrimitiveWrapper() : oc; 164 ClassInfo oc2i = ClassInfo.of(oc2); 165 166 final MethodInfo createMethod = oc2i.getPublicMethod( 167 x -> x.isStatic() 168 && x.isNotDeprecated() 169 && x.hasReturnType(oc2) 170 && x.hasParamTypes(ic) 171 && (x.hasName("forName") || isStaticCreateMethodName(x, ic)) 172 ); 173 174 if (oc2.isEnum() && createMethod == null) { 175 return new Mutater<String,Object>() { 176 @Override 177 public Object mutate(Object outer, String in) { 178 return Enum.valueOf((Class<? extends Enum>)oc2, in); 179 } 180 }; 181 } 182 183 if (createMethod != null) { 184 return new Mutater<String,Object>() { 185 @Override 186 public Object mutate(Object outer, String in) { 187 try { 188 return createMethod.invoke(null, in); 189 } catch (Exception e) { 190 throw asRuntimeException(e); 191 } 192 } 193 }; 194 } 195 } else { 196 MethodInfo createMethod = oci.getPublicMethod( 197 x -> x.isStatic() 198 && x.isNotDeprecated() 199 && x.hasReturnType(oc) 200 && x.hasParamTypes(ic) 201 && isStaticCreateMethodName(x, ic) 202 ); 203 204 if (createMethod != null) { 205 Method cm = createMethod.inner(); 206 return new Mutater() { 207 @Override 208 public Object mutate(Object context, Object in) { 209 try { 210 return cm.invoke(null, in); 211 } catch (Exception e) { 212 throw asRuntimeException(e); 213 } 214 } 215 }; 216 } 217 } 218 219 ConstructorInfo c = oci.getPublicConstructor(x -> x.hasParamTypes(ic)); 220 if (c != null && c.isNotDeprecated()) { 221 boolean isMemberClass = oci.isNonStaticMemberClass(); 222 return new Mutater() { 223 @Override 224 public Object mutate(Object outer, Object in) { 225 try { 226 if (isMemberClass) 227 return c.invoke(outer, in); 228 return c.invoke(in); 229 } catch (Exception e) { 230 throw asRuntimeException(e); 231 } 232 } 233 }; 234 } 235 236 MethodInfo toXMethod = findToXMethod(ici, oci); 237 if (toXMethod != null) { 238 return new Mutater() { 239 @Override 240 public Object mutate(Object outer, Object in) { 241 try { 242 return toXMethod.invoke(in); 243 } catch (Exception e) { 244 throw asRuntimeException(e); 245 } 246 } 247 }; 248 } 249 250 return NULL; 251 } 252 253 private static boolean isStaticCreateMethodName(MethodInfo mi, Class<?> ic) { 254 String n = mi.getSimpleName(), cn = ic.getSimpleName(); 255 return isOneOf(n, "create","from","fromValue","parse","valueOf") 256 || (n.startsWith("from") && n.substring(4).equals(cn)) 257 || (n.startsWith("for") && n.substring(3).equals(cn)) 258 || (n.startsWith("parse") && n.substring(5).equals(cn)); 259 } 260 261 /** 262 * Constructs a new instance of the specified class from the specified string. 263 * 264 * <p> 265 * Class must be one of the following: 266 * <ul> 267 * <li>Have a public constructor that takes in a single <c>String</c> argument. 268 * <li>Have a static <c>fromString(String)</c> (or related) method. 269 * <li>Be an <c>enum</c>. 270 * </ul> 271 * 272 * @param <T> The class type. 273 * @param c The class type. 274 * @param s The string to create the instance from. 275 * @return A new object instance, or <jk>null</jk> if a method for converting the string to an object could not be found. 276 */ 277 public static <T> T fromString(Class<T> c, String s) { 278 Mutater<String,T> t = get(String.class, c); 279 return t == null ? null : t.mutate(s); 280 } 281 282 /** 283 * Converts an object to a string. 284 * 285 * <p> 286 * Normally, this is just going to call <c>toString()</c> on the object. 287 * However, the {@link Locale} and {@link TimeZone} objects are treated special so that the returned value 288 * works with the {@link #fromString(Class, String)} method. 289 * 290 * @param o The object to convert to a string. 291 * @return The stringified object, or <jk>null</jk> if the object was <jk>null</jk>. 292 */ 293 @SuppressWarnings({ "unchecked" }) 294 public static String toString(Object o) { 295 if (o == null) 296 return null; 297 Mutater<Object,String> t = (Mutater<Object,String>)get(o.getClass(), String.class); 298 return t == null ? o.toString() : t.mutate(o); 299 } 300 301 private static MethodInfo findToXMethod(ClassInfo ic, ClassInfo oc) { 302 String tn = oc.getReadableName(); 303 return ic.getPublicMethod( 304 x -> x.isNotStatic() 305 && x.hasNoParams() 306 && x.getSimpleName().startsWith("to") 307 && x.getSimpleName().substring(2).equalsIgnoreCase(tn) 308 ); 309 } 310}