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