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.swap;
014
015import static org.apache.juneau.internal.ClassUtils.*;
016import static org.apache.juneau.internal.CollectionUtils.*;
017
018import java.lang.reflect.*;
019import java.util.*;
020
021import org.apache.juneau.*;
022import org.apache.juneau.annotation.*;
023import org.apache.juneau.parser.*;
024import org.apache.juneau.reflect.*;
025import org.apache.juneau.serializer.*;
026
027/**
028 * A dynamic object swap based on reflection of a Java class that converts Objects to serializable Maps.
029 *
030 * <p>
031 * Looks for methods on the class that can be called to swap-in surrogate Map objects before serialization and swap-out
032 * surrogate Map objects after parsing.
033 *
034 * <h5 class='figure'>Valid surrogate objects</h5>
035 * <ul>
036 *    <li class='jc'>Any subclass of {@link Map}
037 * </ul>
038 *
039 * <h5 class='figure'>Valid swap methods (S = Swapped type)</h5>
040 * <ul>
041 *    <li class='jm'><c><jk>public</jk> S toMap()</c>
042 *    <li class='jm'><c><jk>public</jk> S toMap(BeanSession)</c>
043 *    <li class='jm'><c><jk>public</jk> S toJsonMap()</c>
044 *    <li class='jm'><c><jk>public</jk> S toJsonMap(BeanSession)</c>
045 * </ul>
046 *
047 * <h5 class='figure'>Valid unswap methods (N = Normal type, S = Swapped type)</h5>
048 * <ul>
049 *    <li class='jm'><c><jk>public static</jk> N fromMap(S)</c>
050 *    <li class='jm'><c><jk>public static</jk> N fromMap(BeanSession, S)</c>
051 *    <li class='jm'><c><jk>public static</jk> N fromJsonMap(S)</c>
052 *    <li class='jm'><c><jk>public static</jk> N fromJsonMap(BeanSession, S)</c>
053 *    <li class='jm'><c><jk>public static</jk> N create(S)</c>
054 *    <li class='jm'><c><jk>public static</jk> N create(BeanSession, S)</c>
055 *    <li class='jm'><c><jk>public static</jk> N valueOf(S)</c>
056 *    <li class='jm'><c><jk>public static</jk> N valueOf(BeanSession, S)</c>
057 *    <li class='jm'><c><jk>public</jk> N(S)</c>
058 * </ul>
059 *
060 * <p>
061 * Classes are ignored if any of the following are true:
062 * <ul>
063 *    <li>Classes annotated with {@link BeanIgnore @BeanIgnore}.
064 *    <li>Non-static member classes.
065 * </ul>
066 *
067 * <p>
068 * Members/constructors are ignored if any of the following are true:
069 * <ul>
070 *    <li>Members/constructors annotated with {@link BeanIgnore @BeanIgnore}.
071 *    <li>Deprecated members/constructors.
072 * </ul>
073 *
074 * <h5 class='section'>See Also:</h5><ul>
075 *    <li class='link'><a class="doclink" href="../../../../index.html#jm.Swaps">Swaps</a>
076 * </ul>
077 *
078 * @param <T> The normal class type.
079 */
080public class AutoMapSwap<T> extends ObjectSwap<T,Map<?,?>> {
081
082   private static final Set<String>
083      SWAP_METHOD_NAMES = uset("toMap", "toJsonMap"),
084      UNSWAP_METHOD_NAMES = uset("fromMap", "fromJsonMap", "create", "valueOf");
085
086   /**
087    * Look for constructors and methods on this class and construct a dynamic swap if it's possible to do so.
088    *
089    * @param bc The bean context to use for looking up annotations.
090    * @param ci The class to try to constructor a dynamic swap on.
091    * @return An object swap instance, or <jk>null</jk> if one could not be created.
092    */
093   @SuppressWarnings({ "rawtypes" })
094   public static ObjectSwap<?,?> find(BeanContext bc, ClassInfo ci) {
095
096      if (shouldIgnore(bc, ci))
097         return null;
098
099      // Find swap() method if present.
100      for (MethodInfo m : ci.getMethods()) {
101         if (isSwapMethod(bc, m)) {
102
103            ClassInfo rt = m.getReturnType();
104
105            MethodInfo mi = ci.getMethod(x -> isUnswapMethod(bc, x, ci, rt));
106            if (mi != null)
107               return new AutoMapSwap(bc, ci, m, mi, null);
108
109            ConstructorInfo cs = ci.getDeclaredConstructor(x -> isUnswapConstructor(bc, x, rt));
110            if (cs != null)
111               return new AutoMapSwap(bc, ci, m, null, cs);
112
113            return new AutoMapSwap(bc, ci, m, null, null);
114         }
115      }
116
117      return null;
118   }
119
120   private static boolean shouldIgnore(BeanContext bc, ClassInfo ci) {
121      return
122         ci.hasAnnotation(bc, BeanIgnore.class)
123         || ci.isNonStaticMemberClass();
124   }
125
126   private static boolean isSwapMethod(BeanContext bc, MethodInfo mi) {
127      return
128         mi.isNotDeprecated()
129         && mi.isNotStatic()
130         && mi.isVisible(bc.getBeanMethodVisibility())
131         && mi.hasName(SWAP_METHOD_NAMES)
132         && mi.hasReturnTypeParent(Map.class)
133         && mi.hasFuzzyParamTypes(BeanSession.class)
134         && mi.hasNoAnnotation(bc, BeanIgnore.class);
135   }
136
137   private static boolean isUnswapMethod(BeanContext bc, MethodInfo mi, ClassInfo ci, ClassInfo rt) {
138      return
139         mi.isNotDeprecated()
140         && mi.isStatic()
141         && mi.isVisible(bc.getBeanMethodVisibility())
142         && mi.hasName(UNSWAP_METHOD_NAMES)
143         && mi.hasFuzzyParamTypes(BeanSession.class, rt.inner())
144         && mi.hasReturnTypeParent(ci)
145         && mi.hasNoAnnotation(bc, BeanIgnore.class);
146   }
147
148   private static boolean isUnswapConstructor(BeanContext bc, ConstructorInfo cs, ClassInfo rt) {
149      return
150         cs.isNotDeprecated()
151         && cs.isVisible(bc.getBeanConstructorVisibility())
152         && cs.hasMatchingParamTypes(rt)
153         && cs.hasNoAnnotation(bc, BeanIgnore.class);
154   }
155
156   //------------------------------------------------------------------------------------------------------------------
157
158   private final Method swapMethod, unswapMethod;
159   private final Constructor<?> unswapConstructor;
160
161   private AutoMapSwap(BeanContext bc, ClassInfo ci, MethodInfo swapMethod, MethodInfo unswapMethod, ConstructorInfo unswapConstructor) {
162      super(ci.inner(), swapMethod.inner().getReturnType());
163      this.swapMethod = bc.getBeanMethodVisibility().transform(swapMethod.inner());
164      this.unswapMethod = unswapMethod == null ? null : bc.getBeanMethodVisibility().transform(unswapMethod.inner());
165      this.unswapConstructor = unswapConstructor == null ? null : bc.getBeanConstructorVisibility().transform(unswapConstructor.inner());
166   }
167
168   @Override /* ObjectSwap */
169   public Map<?,?> swap(BeanSession session, Object o) throws SerializeException {
170      try {
171         return (Map<?,?>)swapMethod.invoke(o, getMatchingArgs(swapMethod.getParameterTypes(), session));
172      } catch (Exception e) {
173         throw SerializeException.create(e);
174      }
175   }
176
177   @SuppressWarnings("unchecked")
178   @Override /* ObjectSwap */
179   public T unswap(BeanSession session, Map<?,?> o, ClassMeta<?> hint) throws ParseException {
180      try {
181         if (unswapMethod != null)
182            return (T)unswapMethod.invoke(null, getMatchingArgs(unswapMethod.getParameterTypes(), session, o));
183         if (unswapConstructor != null)
184            return (T)unswapConstructor.newInstance(o);
185         return super.unswap(session, o, hint);
186      } catch (Exception e) {
187         throw ParseException.create(e);
188      }
189   }
190}