All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.apache.juneau.transform.PojoSwap Maven / Gradle / Ivy

There is a newer version: 9.0.1
Show newest version
// ***************************************************************************************************************************
// * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements.  See the NOTICE file *
// * distributed with this work for additional information regarding copyright ownership.  The ASF licenses this file        *
// * to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance            *
// * with the License.  You may obtain a copy of the License at                                                              *
// *                                                                                                                         *
// *  http://www.apache.org/licenses/LICENSE-2.0                                                                             *
// *                                                                                                                         *
// * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an  *
// * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the License for the        *
// * specific language governing permissions and limitations under the License.                                              *
// ***************************************************************************************************************************
package org.apache.juneau.transform;

import static org.apache.juneau.internal.ClassUtils.*;

import java.util.*;

import org.apache.juneau.*;
import org.apache.juneau.annotation.*;
import org.apache.juneau.http.*;
import org.apache.juneau.parser.*;
import org.apache.juneau.serializer.*;

/**
 * Used to swap out non-serializable objects with serializable replacements during serialization, and vis-versa during
 * parsing.
 *
 *
 * 
Description:
* *

* PojoSwaps are used to extend the functionality of the serializers and parsers to be able to handle * POJOs that aren't automatically handled by the serializers or parsers. *
For example, JSON does not have a standard representation for rendering dates. * By defining a special {@code Date} swap and associating it with a serializer and parser, you can convert a * {@code Date} object to a {@code String} during serialization, and convert that {@code String} object back into a * {@code Date} object during parsing. * *

* Swaps MUST declare a public no-arg constructor so that the bean context can instantiate them. * *

* PojoSwaps are associated with instances of {@link BeanContext BeanContexts} by passing the swap * class to the {@link SerializerBuilder#pojoSwaps(Class...)} and {@link ParserBuilder#pojoSwaps(Class...)} methods. *
When associated with a bean context, fields of the specified type will automatically be converted when the * {@link BeanMap#get(Object)} or {@link BeanMap#put(String, Object)} methods are called. * *

* PojoSwaps have two parameters: *

    *
  1. {@code } - The normal representation of an object. *
  2. {@code } - The swapped representation of an object. *
*
{@link Serializer Serializers} use swaps to convert objects of type T into objects of type S, and on calls to * {@link BeanMap#get(Object)}. *
{@link Parser Parsers} use swaps to convert objects of type S into objects of type T, and on calls to * {@link BeanMap#put(String,Object)}. * * *
Subtypes
* * The following abstract subclasses are provided for common swap types: *
    *
  1. {@link StringSwap} - Objects swapped with strings. *
  2. {@link MapSwap} - Objects swapped with {@link ObjectMap ObjectMaps}. *
* * *
Swap Class Type {@code }
* * The swapped object representation of an object must be an object type that the serializers can natively convert to * JSON (or language-specific equivalent). * The list of valid transformed types are as follows... *
    *
  • * {@link String} *
  • * {@link Number} *
  • * {@link Boolean} *
  • * {@link Collection} containing anything on this list. *
  • * {@link Map} containing anything on this list. *
  • * A java bean with properties of anything on this list. *
  • * An array of anything on this list. *
* * *
Normal Class Type {@code }
* * The normal object representation of an object. * * *
Overview
* * The following is an example of a swap that replaces byte arrays with BASE-64 encoded strings: * *

* public class ByteArrayBase64Swap extends PojoSwap<byte[],String> { * * public String swap(BeanSession session, byte[] b) throws SerializeException { * return StringUtils.base64Encode(b); * } * * public byte[] unswap(BeanSession session, String s, ClassMeta<?> hint) throws ParseException { * return StringUtils.base64Decode(s); * } * } *

* *

* WriterSerializer s = JsonSerializer.DEFAULT_LAX.builder().pojoSwaps(ByteArrayBase64Swap.class).build(); * String json = s.serialize(new byte[] {1,2,3}); // Produces "'AQID'" *

* * *
Swap annotation
* *

* Swap classes are often associated directly with POJOs using the {@link Swap @Swap} annotation. * *

* public class MyPojoSwap extends PojoSwap<MyPojo,String> { ... } * * @Swap(MyPojoSwap.class) * public class MyPojo { ... } *

* *

* The @Swap annotation is often simpler since you do not need to tell your serializers and parsers about them * leading to less code. * *

* Swaps can also be associated with getters and setters as well: * *

* @BeanProperty(swap=MyPojo.class) * public MyPojo getMyPojo(); *

* * *
One-way vs. Two-way Serialization
* * Note that while there is a unified interface for handling swaps during both serialization and parsing, * in many cases only one of the {@link #swap(BeanSession, Object)} or {@link #unswap(BeanSession, Object, ClassMeta)} * methods will be defined because the swap is one-way. * For example, a swap may be defined to convert an {@code Iterator} to a {@code ObjectList}, but * it's not possible to unswap an {@code Iterator}. * In that case, the {@code swap(Object}} method would be implemented, but the {@code unswap(ObjectMap)} object would * not, and the swap would be associated on the serializer, but not the parser. * Also, you may choose to serialize objects like {@code Dates} to readable {@code Strings}, in which case it's not * possible to re-parse it back into a {@code Date}, since there is no way for the {@code Parser} to know it's a * {@code Date} from just the JSON or XML text. * * *
Per media-type swaps
*

* The {@link #forMediaTypes()} method can be overridden to provide a set of media types that the swap is invoked on. * It's also possible to define multiple swaps against the same POJO as long as they're differentiated by media type. * When multiple swaps are defined, the best-match media type is used. * *

* In the following example, we define 3 swaps against the same POJO. One for JSON, one for XML, and one for all * other types. * *

* public class PojoSwapTest { * * public static class MyPojo {} * * public static class MyJsonSwap extends PojoSwap<MyPojo,String> { * * public MediaType[] forMediaTypes() { * return MediaType.forStrings("*/json"); * } * * public String swap(BeanSession session, MyPojo o) throws Exception { * return "It's JSON!"; * } * } * * public static class MyXmlSwap extends PojoSwap<MyPojo,String> { * * public MediaType[] forMediaTypes() { * return MediaType.forStrings("*/xml"); * } * * public String swap(BeanSession session, MyPojo o) throws Exception { * return "It's XML!"; * } * } * * public static class MyOtherSwap extends PojoSwap<MyPojo,String> { * * public MediaType[] forMediaTypes() { * return MediaType.forStrings("*/*"); * } * * public String swap(BeanSession session, MyPojo o) throws Exception { * return "It's something else!"; * } * } * * @Test * public void doTest() throws Exception { * * SerializerGroup g = new SerializerGroupBuilder() * .append(JsonSerializer.class, XmlSerializer.class, HtmlSerializer.class) * .sq() * .pojoSwaps(MyJsonSwap.class, MyXmlSwap.class, MyOtherSwap.class) * .build(); * * MyPojo myPojo = new MyPojo(); * * String json = g.getWriterSerializer("text/json").serialize(myPojo); * assertEquals("'It\\'s JSON!'", json); * * String xml = g.getWriterSerializer("text/xml").serialize(myPojo); * assertEquals("<string>It's XML!</string>", xml); * * String html = g.getWriterSerializer("text/html").serialize(myPojo); * assertEquals("<string>It's something else!</string>", html); * } * } *

* *

* Multiple swaps can be associated with a POJO by using the {@link Swaps @Swaps} annotation: * *

* @Swaps( * { * @Swap(MyJsonSwap.class), * @Swap(MyXmlSwap.class), * @Swap(MyOtherSwap.class) * } * ) * public class MyPojo {} *

* *

* Note that since Readers get serialized directly to the output of a serializer, it's possible to * implement a swap that provides fully-customized output. * *

* public class MyJsonSwap extends PojoSwap<MyPojo,Reader> { * * public MediaType[] forMediaTypes() { * return MediaType.forStrings("*/json"); * } * * public Reader swap(BeanSession session, MyPojo o) throws Exception { * return new StringReader("{message:'Custom JSON!'}"); * } * } *

* * *
Templates
* *

* Template strings are arbitrary strings associated with swaps that help provide additional context information * for the swap class. * They're called 'templates' because their primary purpose is for providing template names, such as Apache FreeMarker * template names. * *

* The following is an example of a templated swap class used to serialize POJOs to HTML using FreeMarker: * *

* // Our abstracted templated swap class. * public abstract class FreeMarkerSwap extends PojoSwap<Object,Reader> { * * public MediaType[] forMediaTypes() { * return MediaType.forStrings("*/html"); * } * * public Reader swap(BeanSession session, Object o, String template) throws Exception { * return getFreeMarkerReader(template, o); // Some method that creates raw HTML. * } * } * * // An implementation of our templated swap class. * public class MyPojoSwap extends FreeMarkerSwap { * public String withTemplate() { * return "MyPojo.div.ftl"; * } * } *

* *

* In practice however, the template is usually going to be defined on a @Swap annotation like the following * example: * *

* @Swap(impl=FreeMarkerSwap.class, template="MyPojo.div.ftl") * public class MyPojo {} *

* * *
Localization
* * Swaps have access to the session locale and timezone through the {@link BeanSession#getLocale()} and * {@link BeanSession#getTimeZone()} methods. * This allows you to specify localized swap values when needed. * If using the REST server API, the locale and timezone are set based on the Accept-Language and * Time-Zone headers on the request. * * *
Additional information:
* * See org.apache.juneau.transform for more information. * * @param The normal form of the class. * @param The swapped form of the class. */ @SuppressWarnings({"unchecked","rawtypes"}) public abstract class PojoSwap { /** * Represents a non-existent pojo swap. */ public final static PojoSwap NULL = new PojoSwap((Class)null, (Class)null) {}; private final Class normalClass; private final Class swapClass; private ClassMeta swapClassMeta; // Unfortunately these cannot be made final because we want to allow for PojoSwaps with no-arg constructors // which simplifies declarations. private MediaType[] forMediaTypes; private String template; /** * Constructor. */ protected PojoSwap() { normalClass = (Class)resolveParameterType(PojoSwap.class, 0, this.getClass()); swapClass = resolveParameterType(PojoSwap.class, 1, this.getClass()); forMediaTypes = forMediaTypes(); template = withTemplate(); } /** * Constructor for when the normal and transformed classes are already known. * * @param normalClass The normal class (cannot be serialized). * @param swapClass The transformed class (serializable). */ protected PojoSwap(Class normalClass, Class swapClass) { this.normalClass = normalClass; this.swapClass = swapClass; this.forMediaTypes = forMediaTypes(); this.template = withTemplate(); } /** * Returns the media types that this swap is applicable to. * *

* This method can be overridden to programmatically specify what media types it applies to. * *

* This method is the programmatic equivalent to the {@link Swap#mediaTypes()} annotation. * * @return The media types that this swap is applicable to, or null if it's applicable for all media types. */ public MediaType[] forMediaTypes() { return null; } /** * Returns additional context information for this swap. * *

* Typically this is going to be used to specify a template name, such as a FreeMarker template file name. * *

* This method can be overridden to programmatically specify a template value. * *

* This method is the programmatic equivalent to the {@link Swap#template()} annotation. * * @return Additional context information, or null if not specified. */ public String withTemplate() { return null; } /** * Sets the media types that this swap is associated with. * * @param mediaTypes The media types that this swap is associated with. * @return This object (for method chaining). */ public PojoSwap forMediaTypes(MediaType[] mediaTypes) { this.forMediaTypes = mediaTypes; return this; } /** * Sets the template string on this swap. * * @param template The template string on this swap. * @return This object (for method chaining). */ public PojoSwap withTemplate(String template) { this.template = template; return this; } /** * Returns a number indicating how well this swap matches the specified session. * *

* Uses the {@link MediaType#match(MediaType, boolean)} method algorithm to produce a number whereby a * larger value indicates a "better match". * The idea being that if multiple swaps are associated with a given POJO, we want to pick the best one. * *

* For example, if the session media type is "text/json", then the match values are shown below: * *

    *
  • "text/json" = 100,000 *
  • "*/json" = 5,100 *
  • "*/*" = 5,000 *
  • No media types specified on swap = 1 *
  • "text/xml" = 0 *
* * @param session The bean session. * @return Zero if swap doesn't match the session, or a positive number if it does. */ public int match(BeanSession session) { if (forMediaTypes == null) return 1; int i = 0; MediaType mt = session.getMediaType(); if (forMediaTypes != null) for (MediaType mt2 : forMediaTypes) i = Math.max(i, mt2.match(mt, false)); return i; } /** * If this transform is to be used to serialize non-serializable POJOs, it must implement this method. * *

* The object must be converted into one of the following serializable types: *

    *
  • * {@link String} *
  • * {@link Number} *
  • * {@link Boolean} *
  • * {@link Collection} containing anything on this list. *
  • * {@link Map} containing anything on this list. *
  • * A java bean with properties of anything on this list. *
  • * An array of anything on this list. *
* * @param session * The bean session to use to get the class meta. * This is always going to be the same bean context that created this swap. * @param o The object to be transformed. * @return The transformed object. * @throws Exception If a problem occurred trying to convert the output. */ public S swap(BeanSession session, T o) throws Exception { return swap(session, o, template); } /** * Same as {@link #swap(BeanSession, Object)}, but can be used if your swap has a template associated with it. * * @param session * The bean session to use to get the class meta. * This is always going to be the same bean context that created this swap. * @param o The object to be transformed. * @param template * The template string associated with this swap. * @return The transformed object. * @throws Exception If a problem occurred trying to convert the output. */ public S swap(BeanSession session, T o, String template) throws Exception { throw new SerializeException("Swap method not implemented on PojoSwap ''{0}''", this.getClass().getName()); } /** * If this transform is to be used to reconstitute POJOs that aren't true Java beans, it must implement this method. * * @param session * The bean session to use to get the class meta. * This is always going to be the same bean context that created this swap. * @param f The transformed object. * @param hint * If possible, the parser will try to tell you the object type being created. * For example, on a serialized date, this may tell you that the object being created must be of type * {@code GregorianCalendar}. *
This may be null if the parser cannot make this determination. * @return The narrowed object. * @throws Exception If this method is not implemented. */ public T unswap(BeanSession session, S f, ClassMeta hint) throws Exception { return unswap(session, f, hint, template); } /** * Same as {@link #unswap(BeanSession, Object, ClassMeta)}, but can be used if your swap has a template associated with it. * * @param session * The bean session to use to get the class meta. * This is always going to be the same bean context that created this swap. * @param f The transformed object. * @param hint * If possible, the parser will try to tell you the object type being created. * For example, on a serialized date, this may tell you that the object being created must be of type * {@code GregorianCalendar}. *
This may be null if the parser cannot make this determination. * @param template * The template string associated with this swap. * @return The transformed object. * @throws Exception If a problem occurred trying to convert the output. */ public T unswap(BeanSession session, S f, ClassMeta hint, String template) throws Exception { throw new ParseException("Unswap method not implemented on PojoSwap ''{0}''", this.getClass().getName()); } /** * Returns the T class, the normalized form of the class. * * @return The normal form of this class. */ public Class getNormalClass() { return normalClass; } /** * Returns the G class, the generalized form of the class. * *

* Subclasses must override this method if the generalized class is {@code Object}, meaning it can produce multiple * generalized forms. * * @return The transformed form of this class. */ public Class getSwapClass() { return swapClass; } /** * Returns the {@link ClassMeta} of the transformed class type. * *

* This value is cached for quick lookup. * * @param session * The bean context to use to get the class meta. * This is always going to be the same bean context that created this swap. * @return The {@link ClassMeta} of the transformed class type. */ public ClassMeta getSwapClassMeta(BeanSession session) { if (swapClassMeta == null) swapClassMeta = session.getClassMeta(swapClass); return swapClassMeta; } /** * Checks if the specified object is an instance of the normal class defined on this swap. * * @param o The object to check. * @return * true if the specified object is a subclass of the normal class defined on this transform. * null always return false. */ public boolean isNormalObject(Object o) { if (o == null) return false; return isParentClass(normalClass, o.getClass()); } /** * Checks if the specified object is an instance of the swap class defined on this swap. * * @param o The object to check. * @return * true if the specified object is a subclass of the transformed class defined on this transform. * null always return false. */ public boolean isSwappedObject(Object o) { if (o == null) return false; return isParentClass(swapClass, o.getClass()); } //-------------------------------------------------------------------------------- // Overridden methods //-------------------------------------------------------------------------------- @Override /* Object */ public String toString() { return getClass().getSimpleName() + '<' + getNormalClass().getSimpleName() + "," + getSwapClass().getSimpleName() + '>'; } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy