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}