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.reflect.ReflectionUtils.*;
020import static org.apache.juneau.commons.utils.Shorts.*;
021
022import java.util.*;
023
024import org.apache.juneau.commons.bean.*;
025import org.apache.juneau.commons.http.*;
026import org.apache.juneau.commons.reflect.*;
027import org.apache.juneau.marshall.*;
028import org.apache.juneau.marshall.parser.*;
029import org.apache.juneau.marshall.serializer.*;
030
031/**
032 * Used to swap out non-serializable objects with serializable replacements during serialization, and vis-versa during
033 * parsing.
034 *
035 * <h5 class='topic'>Description</h5>
036 *
037 * <p>
038 * <c>ObjectSwaps</c> are used to extend the functionality of the serializers and parsers to be able to handle
039 * objects that aren't automatically handled by the serializers or parsers.
040 * <br>For example, JSON does not have a standard representation for rendering dates.
041 * By defining a special {@code Date} swap and associating it with a serializer and parser, you can convert a
042 * {@code Date} object to a {@code String} during serialization, and convert that {@code String} object back into a
043 * {@code Date} object during parsing.
044 *
045 * <p>
046 * Swaps MUST declare a public no-arg constructor so that the bean context can instantiate them.
047 *
048 * <p>
049 * <c>ObjectSwaps</c> are associated with serializers and parsers through the following:
050 * <ul class='javatree'>
051 *    <li class='ja'>{@link Swap @Swap}
052 *    <li class='jm'>{@link org.apache.juneau.marshall.MarshallingContext.Builder#swaps(Class...) MarshallingContext.Builder#swaps(Class...)}
053 * </ul>
054 *
055 * <p>
056 * <c>ObjectSwaps</c> have two parameters:
057 * <ol>
058 *    <li>{@code <T>} - The normal representation of an object.
059 *    <li>{@code <S>} - The swapped representation of an object.
060 * </ol>
061 * <br>{@link Serializer Serializers} use swaps to convert objects of type T into objects of type S, and on calls to
062 * {@link BeanMap#get(Object)}.
063 * <br>{@link Parser Parsers} use swaps to convert objects of type S into objects of type T, and on calls to
064 * {@link BeanMap#put(String,Object)}.
065 *
066 * <h5 class='topic'>Swap Class Type {@code <S>}</h5>
067 *
068 * <p>
069 * For normal serialization, the swapped object representation of an object must be an object type that the serializers can natively convert to
070 * JSON (or language-specific equivalent).
071 * <br>The list of valid transformed types are as follows...
072 * <ul>
073 *    <li>
074 *       {@link String}
075 *    <li>
076 *       {@link Number}
077 *    <li>
078 *       {@link Boolean}
079 *    <li>
080 *       {@link Collection} containing anything on this list.
081 *    <li>
082 *       {@link Map} containing anything on this list.
083 *    <li>
084 *       A java bean with properties of anything on this list.
085 *    <li>
086 *       An array of anything on this list.
087 * </ul>
088 *
089 * <p>
090 * For OpenAPI serialization, the valid swapped types also include <code><jk>byte</jk>[]</code> and <c>Calendar</c>.
091 *
092 * <h5 class='topic'>Normal Class Type {@code <T>}</h5>
093 *
094 * <p>
095 * The normal object representation of an object.
096 *
097 * <h5 class='section'>See Also:</h5><ul>
098 *    <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/Swaps">Swap</a>
099
100 * </ul>
101 *
102 * @param <T> The normal form of the class.
103 * @param <S> The swapped form of the class.
104 */
105@SuppressWarnings({
106   "java:S112", // throws Exception intentional - callback/lifecycle method for user implementations
107   "java:S1452", // Wildcard required - ObjectSwap<T,?>, ClassMeta<?> for fluent API and swap metadata
108   "rawtypes", // Raw types necessary for generic type handling
109   "unchecked" // Type erasure requires unchecked casts
110})
111public abstract class ObjectSwap<T,S> {
112
113   /**
114    * Represents a non-existent object swap.
115    */
116   public static final ObjectSwap NULL = new ObjectSwap((Class)null, (Class)null) {};
117
118   private final Class<T> normalClass;
119   private final Class<?> swapClass;
120   private final ClassInfo normalClassInfo;
121   private final ClassInfo swapClassInfo;
122   private ClassMeta<?> swapClassMeta;
123
124   // Unfortunately these cannot be made final because we want to allow for ObjectSwaps with no-arg constructors
125   // which simplifies declarations.
126   private MediaType[] forMediaTypes;
127   private String template;
128
129   /**
130    * Constructor.
131    */
132   protected ObjectSwap() {
133      var ci = info(this.getClass());
134      normalClass = (Class<T>)ci.getParameterType(0, ObjectSwap.class);
135      swapClass = ci.getParameterType(1, ObjectSwap.class);
136      normalClassInfo = info(normalClass);
137      swapClassInfo = info(swapClass);
138      forMediaTypes = forMediaTypes();
139      template = withTemplate();
140   }
141
142   /**
143    * Constructor for when the normal and transformed classes are already known.
144    *
145    * @param normalClass The normal class (cannot be serialized).
146    * @param swapClass The transformed class (serializable).
147    */
148   protected ObjectSwap(Class<T> normalClass, Class<?> swapClass) {
149      this.normalClass = normalClass;
150      this.swapClass = swapClass;
151      normalClassInfo = o(normalClass).map(x -> info(x)).orElse(null);
152      swapClassInfo = o(swapClass).map(x -> info(x)).orElse(null);
153      this.forMediaTypes = forMediaTypes();
154      this.template = withTemplate();
155   }
156
157   /**
158    * Returns the media types that this swap is applicable to.
159    *
160    * <p>
161    * This method can be overridden to programmatically specify what media types it applies to.
162    *
163    * <p>
164    * This method is the programmatic equivalent to the {@link Swap#mediaTypes() @Swap(mediaTypes)} annotation.
165    *
166    * <h5 class='section'>See Also:</h5><ul>
167    *    <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/Swaps#per-media-type-swaps">Per-media-type swaps</a>
168    * </ul>
169    * @return The media types that this swap is applicable to, or <jk>null</jk> if it's applicable for all media types.
170    */
171   @SuppressWarnings({
172      "java:S1168"     // Intentional null = applicable to all media types; callers branch on null (not an empty array).
173   })
174   public MediaType[] forMediaTypes() {
175      return null;
176   }
177
178   /**
179    * Sets the media types that this swap is associated with.
180    *
181    * <h5 class='section'>See Also:</h5><ul>
182    *    <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/Swaps#per-media-type-swaps">Per-media-type swaps</a>
183    * </ul>
184    *
185    * @param mediaTypes The media types that this swap is associated with. Can be <jk>null</jk> to make this swap applicable to all media types.
186    * @return This object.
187    */
188   public ObjectSwap<T,?> forMediaTypes(MediaType[] mediaTypes) {
189      this.forMediaTypes = mediaTypes;
190      return this;
191   }
192
193   /**
194    * Returns the T class, the normalized form of the class.
195    *
196    * @return The normal form of this class.
197    */
198   public ClassInfo getNormalClass() { return normalClassInfo; }
199
200   /**
201    * Returns the G class, the generalized form of the class.
202    *
203    * <p>
204    * Subclasses must override this method if the generalized class is {@code Object}, meaning it can produce multiple
205    * generalized forms.
206    *
207    * @return The transformed form of this class.
208    */
209   public ClassInfo getSwapClass() { return swapClassInfo; }
210
211   /**
212    * Returns the {@link ClassMeta} of the transformed class type.
213    *
214    * <p>
215    * This value is cached for quick lookup.
216    *
217    * @param session
218    *    The bean context to use to get the class meta.
219    *    This is always going to be the same bean context that created this swap.
220    *    Must not be <jk>null</jk> on the first (uncached) call.
221    * @return The {@link ClassMeta} of the transformed class type.
222    */
223   public ClassMeta<?> getSwapClassMeta(MarshallingSession session) {
224      if (swapClassMeta == null)
225         swapClassMeta = session.getClassMeta(swapClass);
226      return swapClassMeta;
227   }
228
229   /**
230    * Checks if the specified object is an instance of the normal class defined on this swap.
231    *
232    * @param o The object to check. Can be <jk>null</jk> (returns <jk>false</jk>).
233    * @return
234    *    <jk>true</jk> if the specified object is a subclass of the normal class defined on this transform.
235    *    A <jk>null</jk> value always returns <jk>false</jk>.
236    */
237   public boolean isNormalObject(Object o) {
238      if (o == null)
239         return false;
240      return normalClassInfo.isAssignableFrom(o.getClass());
241   }
242
243   /**
244    * Checks if the specified object is an instance of the swap class defined on this swap.
245    *
246    * @param o The object to check. Can be <jk>null</jk> (returns <jk>false</jk>).
247    * @return
248    *    <jk>true</jk> if the specified object is a subclass of the transformed class defined on this transform.
249    *    A <jk>null</jk> value always returns <jk>false</jk>.
250    */
251   public boolean isSwappedObject(Object o) {
252      if (o == null)
253         return false;
254      return swapClassInfo.isAssignableFrom(o.getClass());
255   }
256
257   /**
258    * Returns a number indicating how well this swap matches the specified session.
259    *
260    * <p>
261    * Uses the {@link MediaType#match(MediaType, boolean)} method algorithm to produce a number whereby a
262    * larger value indicates a "better match".
263    * The idea being that if multiple swaps are associated with a given object, we want to pick the best one.
264    *
265    * <p>
266    * For example, if the session media type is <js>"text/json"</js>, then the match values are shown below:
267    *
268    * <ul>
269    *    <li><js>"text/json"</js> = <c>100,000</c>
270    *    <li><js>"&#42;/json"</js> = <c>5,100</c>
271    *    <li><js>"&#42;/&#42;"</js> = <c>5,000</c>
272    *    <li>No media types specified on swap = <c>1</c>
273    *    <li><js>"text/xml"</js> = <c>0</c>
274    * </ul>
275    *
276    * @param session The bean session. Can be <jk>null</jk> (returns <c>0</c>, or <c>1</c> if this swap has no media types configured).
277    * @return Zero if swap doesn't match the session, or a positive number if it does.
278    */
279   public int match(MarshallingSession session) {
280      if (forMediaTypes == null)
281         return 1;
282      int i = 0;
283      var mt = session != null ? session.getMediaType() : null;
284      if (mt == null)
285         return 0;
286      if (nn(forMediaTypes))
287         for (var mt2 : forMediaTypes)
288            i = Math.max(i, mt2.match(mt, false));
289      return i;
290   }
291
292   /**
293    * If this transform is to be used to serialize non-serializable objects, it must implement this method.
294    *
295    * <p>
296    * The object must be converted into one of the following serializable types:
297    * <ul class='spaced-list'>
298    *    <li>
299    *       {@link String}
300    *    <li>
301    *       {@link Number}
302    *    <li>
303    *       {@link Boolean}
304    *    <li>
305    *       {@link Collection} containing anything on this list.
306    *    <li>
307    *       {@link Map} containing anything on this list.
308    *    <li>
309    *       A java bean with properties of anything on this list.
310    *    <li>
311    *       An array of anything on this list.
312    * </ul>
313    *
314    * @param session
315    *    The bean session to use to get the class meta.
316    *    This is always going to be the same bean context that created this swap.
317    * @param o The object to be transformed.
318    * @return The transformed object.
319    * @throws Exception If a problem occurred trying to convert the output.
320    */
321   public S swap(MarshallingSession session, T o) throws Exception {
322      return swap(session, o, template);
323   }
324
325   /**
326    * Same as {@link #swap(MarshallingSession, Object)}, but can be used if your swap has a template associated with it.
327    *
328    * @param session
329    *    The bean session to use to get the class meta.
330    *    This is always going to be the same bean context that created this swap.
331    * @param o The object to be transformed.
332    * @param template
333    *    The template string associated with this swap.
334    * @return The transformed object.
335    * @throws Exception If a problem occurred trying to convert the output.
336    */
337   public S swap(MarshallingSession session, T o, String template) throws Exception {
338      throw new SerializeException("Swap method not implemented on ObjectSwap '%s'", cn(this));
339   }
340
341   @Override /* Overridden from Object */
342   public String toString() {
343      return cns(this) + '<' + getNormalClass().getNameSimple() + "," + getSwapClass().getNameSimple() + '>';
344   }
345
346   /**
347    * If this transform is to be used to reconstitute objects that aren't true Java beans, it must implement this method.
348    *
349    * @param session
350    *    The bean session to use to get the class meta.
351    *    This is always going to be the same bean context that created this swap.
352    * @param f The transformed object.
353    * @param hint
354    *    If possible, the parser will try to tell you the object type being created.
355    *    For example, on a serialized date, this may tell you that the object being created must be of type
356    *    {@code GregorianCalendar}.
357    *    <br>This may be <jk>null</jk> if the parser cannot make this determination.
358    * @return The narrowed object.
359    * @throws Exception If this method is not implemented.
360    */
361   public T unswap(MarshallingSession session, S f, ClassMeta<?> hint) throws Exception {
362      return unswap(session, f, hint, template);
363   }
364
365   /**
366    * Same as {@link #unswap(MarshallingSession, Object, ClassMeta)}, but can be used if your swap has a template associated with it.
367    *
368    * @param session
369    *    The bean session to use to get the class meta.
370    *    This is always going to be the same bean context that created this swap.
371    * @param f The transformed object.
372    * @param hint
373    *    If possible, the parser will try to tell you the object type being created.
374    *    For example, on a serialized date, this may tell you that the object being created must be of type
375    *    {@code GregorianCalendar}.
376    *    <br>This may be <jk>null</jk> if the parser cannot make this determination.
377    * @param template
378    *    The template string associated with this swap.
379    * @return The transformed object.
380    * @throws Exception If a problem occurred trying to convert the output.
381    */
382   public T unswap(MarshallingSession session, S f, ClassMeta<?> hint, String template) throws Exception {
383      throw new ParseException("Unswap method not implemented on ObjectSwap '%s'", cn(this));
384   }
385
386   /**
387    * Returns additional context information for this swap.
388    *
389    * <p>
390    * Typically this is going to be used to specify a template name, such as a FreeMarker template file name.
391    *
392    * <p>
393    * This method can be overridden to programmatically specify a template value.
394    *
395    * <p>
396    * This method is the programmatic equivalent to the {@link Swap#template() @Swap(template)} annotation.
397    *
398    * <h5 class='section'>See Also:</h5><ul>
399    *    <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/TemplatedSwaps">Templated Swaps</a>
400    * </ul>
401    *
402    * @return Additional context information, or <jk>null</jk> if not specified.
403    */
404   public String withTemplate() {
405      return null;
406   }
407
408   /**
409    * Sets the template string on this swap.
410    *
411    * <h5 class='section'>See Also:</h5><ul>
412    *    <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/TemplatedSwaps">Templated Swaps</a>
413    * </ul>
414    *
415    * @param template The template string on this swap. Can be <jk>null</jk> to unset the template.
416    * @return This object.
417    */
418   public ObjectSwap<T,?> withTemplate(String template) {
419      this.template = template;
420      return this;
421   }
422}