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