org.apache.juneau.transform.PojoSwap Maven / Gradle / Ivy
// ***************************************************************************************************************************
// * 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:
*
* - {@code
} - The normal representation of an object.
* - {@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:
*
* - {@link StringSwap} - Objects swapped with strings.
*
- {@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() + '>';
}
}