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