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}