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}