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.conversion.*;
028import org.apache.juneau.commons.reflect.*;
029import org.apache.juneau.marshall.*;
030import org.apache.juneau.marshall.parser.*;
031import org.apache.juneau.marshall.serializer.*;
032
033/**
034 * A dynamic object swap based on reflection of a Java class that converts Objects to Number serializable objects.
035 *
036 * <p>
037 * Looks for methods on the class that can be called to swap-in surrogate Number objects before serialization and swap-out
038 * surrogate Number objects after parsing.
039 *
040 * <h5 class='figure'>Valid surrogate objects</h5>
041 * <ul>
042 *    <li class='jc'>Any subclass of {@link Number}
043 *    <li class='jc'>Any number primitive
044 * </ul>
045 *
046 * <h5 class='figure'>Valid swap methods (S = Swapped type)</h5>
047 * <ul>
048 *    <li class='jm'><c><jk>public</jk> S toNumber()</c>
049 *    <li class='jm'><c><jk>public</jk> S toNumber(MarshallingSession)</c>
050 *    <li class='jm'><c><jk>public</jk> S toInteger()</c>
051 *    <li class='jm'><c><jk>public</jk> S toInteger(MarshallingSession)</c>
052 *    <li class='jm'><c><jk>public</jk> S toInt()</c>
053 *    <li class='jm'><c><jk>public</jk> S toInt(MarshallingSession)</c>
054 *    <li class='jm'><c><jk>public</jk> S toLong()</c>
055 *    <li class='jm'><c><jk>public</jk> S toLong(MarshallingSession)</c>
056 *    <li class='jm'><c><jk>public</jk> S toFloat()</c>
057 *    <li class='jm'><c><jk>public</jk> S toFloat(MarshallingSession)</c>
058 *    <li class='jm'><c><jk>public</jk> S toDouble()</c>
059 *    <li class='jm'><c><jk>public</jk> S toDouble(MarshallingSession)</c>
060 *    <li class='jm'><c><jk>public</jk> S toShort()</c>
061 *    <li class='jm'><c><jk>public</jk> S toShort(MarshallingSession)</c>
062 *    <li class='jm'><c><jk>public</jk> S toByte()</c>
063 *    <li class='jm'><c><jk>public</jk> S toByte(MarshallingSession)</c>
064 * </ul>
065 *
066 * <h5 class='figure'>Valid unswap methods (N = Normal type, S = Swapped type)</h5>
067 * <ul>
068 *    <li class='jm'><c><jk>public static</jk> N fromInteger(S)</c>
069 *    <li class='jm'><c><jk>public static</jk> N fromInteger(MarshallingSession, S)</c>
070 *    <li class='jm'><c><jk>public static</jk> N fromInt(S)</c>
071 *    <li class='jm'><c><jk>public static</jk> N fromInt(MarshallingSession, S)</c>
072 *    <li class='jm'><c><jk>public static</jk> N fromLong(S)</c>
073 *    <li class='jm'><c><jk>public static</jk> N fromLong(MarshallingSession, S)</c>
074 *    <li class='jm'><c><jk>public static</jk> N fromFloat(S)</c>
075 *    <li class='jm'><c><jk>public static</jk> N fromFloat(MarshallingSession, S)</c>
076 *    <li class='jm'><c><jk>public static</jk> N fromDouble(S)</c>
077 *    <li class='jm'><c><jk>public static</jk> N fromDouble(MarshallingSession, S)</c>
078 *    <li class='jm'><c><jk>public static</jk> N fromShort(S)</c>
079 *    <li class='jm'><c><jk>public static</jk> N fromShort(MarshallingSession, S)</c>
080 *    <li class='jm'><c><jk>public static</jk> N fromByte(S)</c>
081 *    <li class='jm'><c><jk>public static</jk> N fromByte(MarshallingSession, S)</c>
082 *    <li class='jm'><c><jk>public static</jk> N create(S)</c>
083 *    <li class='jm'><c><jk>public static</jk> N create(MarshallingSession, S)</c>
084 *    <li class='jm'><c><jk>public static</jk> N valueOf(S)</c>
085 *    <li class='jm'><c><jk>public static</jk> N valueOf(MarshallingSession, S)</c>
086 *    <li class='jm'><c><jk>public</jk> N(S)</c>
087 * </ul>
088 * <p>
089 * Classes are ignored if any of the following are true:
090 * <ul>
091 *    <li>Classes annotated with {@link BeanIgnore @BeanIgnore}.
092 *    <li>Non-static member classes.
093 * </ul>
094 *
095 * <p>
096 * Members/constructors are ignored if any of the following are true:
097 * <ul>
098 *    <li>Members/constructors annotated with {@link BeanIgnore @BeanIgnore}.
099 *    <li>Deprecated members/constructors.
100 * </ul>
101 *
102 * <h5 class='section'>See Also:</h5><ul>
103 *    <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/Swaps">Swap</a>
104 * </ul>
105 *
106 * @param <T> The normal class type.
107 */
108@SuppressWarnings({
109   "java:S3776" // The long &&/|| condition chains in isSwapMethod()/isUnswapMethod() and the nested loops in the constructor.
110})
111public class AutoNumberSwap<T> extends ObjectSwap<T,Number> {
112
113   private static final Set<String> SWAP_METHOD_NAMES = u(set("toNumber", "toInteger", "toInt", "toLong", "toFloat", "toDouble", "toShort", "toByte"));
114   private static final Set<String> UNSWAP_METHOD_NAMES = u(set("fromInteger", "fromInt", "fromLong", "fromFloat", "fromDouble", "fromShort", "fromByte", "create", "valueOf"));
115
116   /**
117    * Look for constructors and methods on this class and construct a dynamic swap if it's possible to do so.
118    *
119    * @param bc The bean context to use for looking up annotations. Must not be <jk>null</jk>.
120    * @param ci The class to try to constructor a dynamic swap on. Must not be <jk>null</jk>.
121    * @return An object swap instance, or <jk>null</jk> if one could not be created.
122    */
123   @SuppressWarnings({
124      "java:S1452", // Wildcard required - ObjectSwap<?,?> for dynamically discovered swap types
125      "rawtypes" // Raw types necessary for generic type handling
126   })
127   public static ObjectSwap<?,?> find(MarshallingContext bc, ClassInfo ci) {
128
129      if (shouldIgnore(bc, ci))
130         return null;
131
132      // Find swap() method if present.
133      for (var m : ci.getAllMethods()) {
134
135         if (isSwapMethod(bc, m)) {
136
137            var rt = m.getReturnType();
138
139            var mi = ci.getMethod(x -> isUnswapMethod(bc, x, ci, rt)).orElse(null);
140            if (nn(mi))
141               return new AutoNumberSwap(bc, ci, m, mi, null);
142
143            var cs = ci.getDeclaredConstructor(x -> isUnswapConstructor(bc, x, rt)).orElse(null);
144            if (nn(cs))
145               return new AutoNumberSwap(bc, ci, m, null, cs);
146
147            return new AutoNumberSwap(bc, ci, m, null, null);
148         }
149      }
150
151      return null;
152   }
153
154   private static boolean isSwapMethod(MarshallingContext bc, MethodInfo mi) {
155      var rt = mi.getReturnType();
156      // @formatter:off
157      return
158         mi.isNotDeprecated()
159         && mi.isNotStatic()
160         && mi.isVisible(bc.getBeanMethodVisibility())
161         && (rt.isAssignableTo(Number.class) || (rt.isPrimitive() && rt.isAny(int.class, short.class, long.class, float.class, double.class, byte.class)))
162         && mi.hasAnyName(SWAP_METHOD_NAMES)
163         && mi.hasParameterTypesLenient(MarshallingSession.class)
164         && mi.getMatchingMethods().stream().noneMatch(m2 -> bc.getAnnotationProvider().has(BeanIgnore.class, m2));
165      // @formatter:on
166   }
167
168   private static boolean isUnswapConstructor(MarshallingContext bc, ConstructorInfo cs, ClassInfo rt) {
169      // @formatter:off
170      return
171         cs.isNotDeprecated()
172         && cs.isVisible(bc.getBeanConstructorVisibility())
173         && cs.hasParameterTypeParents(rt)
174         && ! bc.getAnnotationProvider().has(BeanIgnore.class, cs);
175      // @formatter:on
176   }
177
178   private static boolean isUnswapMethod(MarshallingContext bc, MethodInfo mi, ClassInfo ci, ClassInfo rt) {
179      // @formatter:off
180      return
181         mi.isNotDeprecated()
182         && mi.isStatic()
183         && mi.isVisible(bc.getBeanMethodVisibility())
184         && mi.hasAnyName(UNSWAP_METHOD_NAMES)
185         && mi.hasParameterTypesLenient(MarshallingSession.class, rt.inner())
186         && mi.hasReturnTypeParent(ci)
187         && mi.getMatchingMethods().stream().noneMatch(m2 -> bc.getAnnotationProvider().has(BeanIgnore.class, m2));
188      // @formatter:on
189   }
190
191   private static boolean shouldIgnore(MarshallingContext bc, ClassInfo ci) {
192      // @formatter:off
193      return
194         ci.isNonStaticMemberClass()
195         || ci.isPrimitive()
196         || ci.isAssignableTo(Number.class)
197         || bc.getAnnotationProvider().has(BeanIgnore.class, ci);
198      // @formatter:on
199   }
200
201   //------------------------------------------------------------------------------------------------------------------
202
203   private final Method swapMethod;
204   private final Method unswapMethod;
205   private final Constructor<?> unswapConstructor;
206   private final Class<?> unswapType;
207
208   @SuppressWarnings({
209      "null" // Method info variables are checked for null before use in method body
210   })
211   private AutoNumberSwap(MarshallingContext bc, ClassInfo ci, MethodInfo swapMethod, MethodInfo unswapMethod, ConstructorInfo unswapConstructor) {
212      super(ci.inner(), swapMethod.inner().getReturnType());
213      this.swapMethod = bc.getBeanMethodVisibility().transform(swapMethod.inner());
214      this.unswapMethod = unswapMethod == null ? null : bc.getBeanMethodVisibility().transform(unswapMethod.inner());
215      this.unswapConstructor = unswapConstructor == null ? null : bc.getBeanConstructorVisibility().transform(unswapConstructor.inner());
216
217      Class<?> unswapType2 = null;
218      if (nn(unswapMethod)) {
219         for (var pi : unswapMethod.getParameters())
220            if (! pi.getParameterType().is(MarshallingSession.class)) {
221               var wrap = pi.getParameterType().getWrapperIfPrimitive();
222               if (nn(wrap))
223                  unswapType2 = wrap.inner();
224            }
225      } else if (nn(unswapConstructor)) {
226         for (var pi : unswapConstructor.getParameters())
227            if (! pi.getParameterType().is(MarshallingSession.class)) {
228               var wrap = pi.getParameterType().getWrapperIfPrimitive();
229               if (nn(wrap))
230                  unswapType2 = wrap.inner();
231            }
232      }
233      this.unswapType = unswapType2;
234   }
235
236   @Override /* Overridden from ObjectSwap */
237   public Number swap(MarshallingSession session, Object o) throws SerializeException {
238      try {
239         return (Number)swapMethod.invoke(o, getMatchingArgs(swapMethod.getParameterTypes(), session));
240      } catch (Exception e) {
241         throw SerializeException.create(e);
242      }
243   }
244
245   @SuppressWarnings({
246      "unchecked" // Type erasure requires cast to T
247   })
248   @Override /* Overridden from ObjectSwap */
249   public T unswap(MarshallingSession session, Number o, ClassMeta<?> hint) throws ParseException {
250      if (unswapType == null)
251         throw new ParseException("No unparse methodology found for object.");
252      try {
253         Object o2 = BasicConverter.INSTANCE.to(o, unswapType);
254         if (nn(unswapMethod))
255            return (T)unswapMethod.invoke(null, getMatchingArgs(unswapMethod.getParameterTypes(), session, o2));
256         if (nn(unswapConstructor))
257            return (T)unswapConstructor.newInstance(o2);
258         return super.unswap(session, o, hint);
259      } catch (Exception e) {
260         throw ParseException.create(e);
261      }
262   }
263}