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