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