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.internal;
014
015import java.util.concurrent.*;
016
017import static org.apache.juneau.internal.ClassUtils.*;
018import static org.apache.juneau.internal.ClassFlags.*;
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 */
026public class TransformCache {
027   private static final ConcurrentHashMap<Class<?>,Map<Class<?>,Transform<?,?>>> CACHE = new ConcurrentHashMap<>();
028
029   /**
030    * Represents a non-existent transform.
031    */
032   public static final Transform<Object,Object> NULL = new Transform<Object,Object>() {
033      @Override
034      public Object transform(Object outer, Object in) {
035         return null;
036      }
037   };
038
039   // Special cases.
040   static {
041
042      // TimeZone doesn't follow any standard conventions.
043      add(String.class, TimeZone.class,
044         new Transform<String,TimeZone>() {
045            @Override public TimeZone transform(Object outer, String in) {
046               return TimeZone.getTimeZone(in);
047            }
048         }
049      );
050      add(TimeZone.class, String.class,
051         new Transform<TimeZone,String>() {
052            @Override public String transform(Object outer, TimeZone in) {
053               return in.getID();
054            }
055         }
056      );
057
058      // Locale(String) doesn't work on strings like "ja_JP".
059      add(String.class, Locale.class,
060         new Transform<String,Locale>() {
061            @Override
062            public Locale transform(Object outer, String in) {
063               return Locale.forLanguageTag(in.replace('_', '-'));
064            }
065         }
066      );
067
068      // String-to-Boolean transform should allow for "null" keyword.
069      add(String.class, Boolean.class,
070         new Transform<String,Boolean>() {
071            @Override
072            public Boolean transform(Object outer, String in) {
073               if (in == null || "null".equals(in) || in.isEmpty())
074                  return null;
075               return Boolean.valueOf(in);
076            }
077         }
078      );
079   }
080
081   /**
082    * Adds a transform for the specified input/output types.
083    *
084    * @param ic The input type.
085    * @param oc The output type.
086    * @param t The transform for converting the input to the output.
087    */
088   public static synchronized void add(Class<?> ic, Class<?> oc, Transform<?,?> t) {
089      Map<Class<?>,Transform<?,?>> m = CACHE.get(oc);
090      if (m == null) {
091         m = new ConcurrentHashMap<>();
092         CACHE.put(oc, m);
093      }
094      m.put(ic, t);
095   }
096
097   /**
098    * Returns the transform for converting the specified input type to the specified output type.
099    *
100    * @param ic The input type.
101    * @param oc The output type.
102    * @return The transform for performing the conversion, or <jk>null</jk> if the conversion cannot be made.
103    */
104   @SuppressWarnings({ "unchecked", "rawtypes" })
105   public static <I,O> Transform<I,O> get(final Class<I> ic, final Class<O> oc) {
106
107      if (ic == null || oc == null)
108         return null;
109
110      Map<Class<?>,Transform<?,?>> m = CACHE.get(oc);
111      if (m == null) {
112         m = new ConcurrentHashMap<>();
113         CACHE.putIfAbsent(oc, m);
114         m = CACHE.get(oc);
115      }
116
117      Transform t = m.get(ic);
118      if (t != null)
119         return t == NULL ? null : t;
120
121      for (Iterator<Class<?>> i = ClassUtils.getParentClasses(ic, false, true); i.hasNext(); ) {
122         Class pic = i.next();
123         t = m.get(pic);
124         if (t != null) {
125            m.put(pic, t);
126            return t == NULL ? null : t;
127         }
128      }
129
130      if (ic == oc) {
131         t = new Transform<I,O>() {
132            @Override public O transform(Object outer, I in) {
133               return (O)in;
134            }
135         };
136      } else if (ic == String.class) {
137         final Class<?> oc2 = hasPrimitiveWrapper(oc) ? getPrimitiveWrapper(oc) : oc;
138         if (oc2.isEnum()) {
139            t = new Transform<String,O>() {
140               @Override
141               public O transform(Object outer, String in) {
142                  return (O)Enum.valueOf((Class<? extends Enum>)oc2, in);
143               }
144            };
145         } else {
146            final Method fromStringMethod = findPublicFromStringMethod(oc2);
147            if (fromStringMethod != null) {
148               t = new Transform<String,O>() {
149                  @Override
150                  public O transform(Object outer, String in) {
151                     try {
152                        return (O)fromStringMethod.invoke(null, in);
153                     } catch (Exception e) {
154                        throw new RuntimeException(e);
155                     }
156                  }
157               };
158            }
159         }
160      }
161
162      if (t == null) {
163         Method createMethod = findPublicStaticCreateMethod(oc, ic, "create");
164         if (createMethod == null)
165            createMethod = findPublicStaticCreateMethod(oc, ic, "from" + ic.getSimpleName());
166         if (createMethod != null) {
167            final Method cm = createMethod;
168            t = new Transform<I,O>() {
169               @Override
170               public O transform(Object context, I in) {
171                  try {
172                     return (O)cm.invoke(null, in);
173                  } catch (Exception e) {
174                     throw new RuntimeException(e);
175                  }
176               }
177            };
178         } else {
179            final Constructor<?> c = findPublicConstructor(oc, ic);
180            final boolean isMemberClass = oc.isMemberClass() && ! isStatic(oc);
181            if (c != null && ! isDeprecated(c)) {
182               t = new Transform<I,O>() {
183                  @Override
184                  public O transform(Object outer, I in) {
185                     try {
186                        if (isMemberClass)
187                           return (O)c.newInstance(outer, in);
188                        return (O)c.newInstance(in);
189                     } catch (Exception e) {
190                        throw new RuntimeException(e);
191                     }
192                  }
193               };
194            }
195
196         }
197      }
198
199      if (t == null) {
200         for (Method m2 : getAllMethods(ic, false)) {
201            if (isAll(m2, PUBLIC, NOT_STATIC, HAS_NO_ARGS, NOT_DEPRECATED) && m2.getName().startsWith("to") && m2.getReturnType() == oc) {
202               final Method m3 = m2;
203               t = new Transform<I,O>() {
204                  @Override
205                  public O transform(Object outer, I in) {
206                     try {
207                        return (O)m3.invoke(in);
208                     } catch (Exception e) {
209                        throw new RuntimeException(e);
210                     }
211                  }
212               };
213               break;
214            }
215         }
216      }
217      if (t == null)
218         t = NULL;
219
220      m.put(ic, t);
221
222      return t == NULL ? null : t;
223   }
224}