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