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