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

org.apache.juneau.serializer.SerializerSession 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.serializer;

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

import java.io.*;
import java.lang.reflect.*;
import java.text.*;
import java.util.*;

import org.apache.juneau.*;
import org.apache.juneau.internal.*;
import org.apache.juneau.parser.*;
import org.apache.juneau.soap.*;
import org.apache.juneau.transform.*;

/**
 * Serializer session that lives for the duration of a single use of {@link Serializer}.
 *
 * 

* Used by serializers for the following purposes: *

    *
  • * Keeping track of how deep it is in a model for indentation purposes. *
  • * Ensuring infinite loops don't occur by setting a limit on how deep to traverse a model. *
  • * Ensuring infinite loops don't occur from loops in the model (when detectRecursions is enabled. *
  • * Allowing serializer properties to be overridden on method calls. *
* *

* This class is NOT thread safe. * It is typically discarded after one-time use although it can be reused within the same thread. */ public abstract class SerializerSession extends BeanTraverseSession { private final Serializer ctx; private final UriResolver uriResolver; private final Method javaMethod; // Java method that invoked this serializer. // Writable properties private final boolean useWhitespace; private final SerializerListener listener; /** * Create a new session using properties specified in the context. * * @param ctx * The context creating this session object. * The context contains all the configuration settings for this object. * Can be null. * @param args * Runtime arguments. * These specify session-level information such as locale and URI context. * It also include session-level properties that override the properties defined on the bean and * serializer contexts. */ protected SerializerSession(Serializer ctx, SerializerSessionArgs args) { super(ctx, args == null ? SerializerSessionArgs.DEFAULT : args); this.ctx = ctx; args = args == null ? SerializerSessionArgs.DEFAULT : args; this.javaMethod = args.javaMethod; this.uriResolver = new UriResolver(ctx.getUriResolution(), ctx.getUriRelativity(), args.uriContext == null ? ctx.getUriContext() : args.uriContext); this.listener = newInstance(SerializerListener.class, ctx.getListener()); this.useWhitespace = args.useWhitespace != null ? args.useWhitespace : ctx.isUseWhitespace(); } /** * Default constructor. * * @param args * Runtime arguments. * These specify session-level information such as locale and URI context. * It also include session-level properties that override the properties defined on the bean and * serializer contexts. */ protected SerializerSession(SerializerSessionArgs args) { this(Serializer.DEFAULT, args); } @Override /* Session */ public ObjectMap asMap() { return super.asMap() .append("SerializerSession", new ObjectMap() .append("uriResolver", uriResolver) ); } /** * Wraps the specified input object into a {@link ParserPipe} object so that it can be easily converted into * a stream or reader. * * @param output * The output location. *
For character-based serializers, this can be any of the following types: *

    *
  • {@link Writer} *
  • {@link OutputStream} - Output will be written as UTF-8 encoded stream. *
  • {@link File} - Output will be written as system-default encoded stream. *
  • {@link StringBuilder} *
*
For byte-based serializers, this can be any of the following types: *
    *
  • {@link OutputStream} *
  • {@link File} *
* @return * A new {@link ParserPipe} wrapper around the specified input object. */ protected SerializerPipe createPipe(Object output) { return new SerializerPipe(output); } //----------------------------------------------------------------------------------------------------------------- // Abstract methods //----------------------------------------------------------------------------------------------------------------- /** * Serializes a POJO to the specified output stream or writer. * *

* This method should NOT close the context object. * * @param pipe Where to send the output from the serializer. * @param o The object to serialize. * @throws Exception If thrown from underlying stream, or if the input contains a syntax error or is malformed. */ protected abstract void doSerialize(SerializerPipe pipe, Object o) throws Exception; /** * Shortcut method for serializing objects directly to either a String or byte[] * depending on the serializer type. * * @param o The object to serialize. * @return * The serialized object. *
Character-based serializers will return a String. *
Stream-based serializers will return a byte[]. * @throws SerializeException If a problem occurred trying to convert the output. */ public abstract Object serialize(Object o) throws SerializeException; /** * Shortcut method for serializing an object to a String. * * @param o The object to serialize. * @return * The serialized object. *
Character-based serializers will return a String *
Stream-based serializers will return a byte[] converted to a string based on the {@link OutputStreamSerializer#OSSERIALIZER_binaryFormat} setting. * @throws SerializeException If a problem occurred trying to convert the output. */ public abstract String serializeToString(Object o) throws SerializeException; /** * Returns true if this serializer subclasses from {@link WriterSerializer}. * * @return true if this serializer subclasses from {@link WriterSerializer}. */ public abstract boolean isWriterSerializer(); //----------------------------------------------------------------------------------------------------------------- // Other methods //----------------------------------------------------------------------------------------------------------------- /** * Serialize the specified object using the specified session. * * @param out Where to send the output from the serializer. * @param o The object to serialize. * @throws SerializeException If a problem occurred trying to convert the output. */ public final void serialize(Object o, Object out) throws SerializeException { try (SerializerPipe pipe = createPipe(out)) { doSerialize(pipe, o); } catch (SerializeException e) { throw e; } catch (StackOverflowError e) { throw new SerializeException(this, "Stack overflow occurred. This can occur when trying to serialize models containing loops. It's recommended you use the BeanTraverseContext.BEANTRAVERSE_detectRecursions setting to help locate the loop.").initCause(e); } catch (Exception e) { throw new SerializeException(this, e); } finally { checkForWarnings(); } } /** * Returns the Java method that invoked this serializer. * *

* When using the REST API, this is the Java method invoked by the REST call. * Can be used to access annotations defined on the method or class. * * @return The Java method that invoked this serializer. */ protected final Method getJavaMethod() { return javaMethod; } /** * Returns the URI resolver. * * @return The URI resolver. */ protected final UriResolver getUriResolver() { return uriResolver; } /** * Specialized warning when an exception is thrown while executing a bean getter. * * @param p The bean map entry representing the bean property. * @param t The throwable that the bean getter threw. */ protected final void onBeanGetterException(BeanPropertyMeta p, Throwable t) { if (listener != null) listener.onBeanGetterException(this, t, p); String prefix = (isDebug() ? getStack(false) + ": " : ""); addWarning("{0}Could not call getValue() on property ''{1}'' of class ''{2}'', exception = {3}", prefix, p.getName(), p.getBeanMeta().getClassMeta(), t.getLocalizedMessage()); } /** * Logs a warning message. * * @param t The throwable that was thrown (if there was one). * @param msg The warning message. * @param args Optional {@link MessageFormat}-style arguments. */ @Override protected void onError(Throwable t, String msg, Object... args) { if (listener != null) listener.onError(this, t, format(msg, args)); super.onError(t, msg, args); } /** * Trims the specified string if {@link SerializerSession#isTrimStrings()} returns true. * * @param o The input string to trim. * @return The trimmed string, or null if the input was null. */ public final String trim(Object o) { if (o == null) return null; String s = o.toString(); if (isTrimStrings()) s = s.trim(); return s; } /** * Generalize the specified object if a POJO swap is associated with it. * * @param o The object to generalize. * @param type The type of object. * @return The generalized object, or null if the object is null. * @throws SerializeException If a problem occurred trying to convert the output. */ @SuppressWarnings({ "rawtypes", "unchecked" }) protected final Object generalize(Object o, ClassMeta type) throws SerializeException { try { if (o == null) return null; PojoSwap f = (type == null || type.isObject() ? getClassMeta(o.getClass()).getPojoSwap(this) : type.getPojoSwap(this)); if (f == null) return o; return f.swap(this, o); } catch (SerializeException e) { throw e; } catch (Exception e) { throw new SerializeException(e); } } /** * Returns true if the specified value should not be serialized. * * @param cm The class type of the object being serialized. * @param attrName The bean attribute name, or null if this isn't a bean attribute. * @param value The object being serialized. * @return true if the specified value should not be serialized. * @throws SerializeException If recursion occurred. */ public final boolean canIgnoreValue(ClassMeta cm, String attrName, Object value) throws SerializeException { if (isTrimNullProperties() && value == null) return true; if (value == null) return false; if (cm == null) cm = object(); if (isTrimEmptyCollections()) { if (cm.isArray() || (cm.isObject() && value.getClass().isArray())) { if (((Object[])value).length == 0) return true; } if (cm.isCollection() || (cm.isObject() && isParentClass(Collection.class, value.getClass()))) { if (((Collection)value).isEmpty()) return true; } } if (isTrimEmptyMaps()) { if (cm.isMap() || (cm.isObject() && isParentClass(Map.class, value.getClass()))) { if (((Map)value).isEmpty()) return true; } } try { if (isTrimNullProperties() && willRecurse(attrName, value, cm)) return true; } catch (BeanRecursionException e) { throw new SerializeException(e); } return false; } /** * Sorts the specified map if {@link SerializerSession#isSortMaps()} returns true. * * @param m The map being sorted. * @return A new sorted {@link TreeMap}. */ public final Map sort(Map m) { if (isSortMaps() && m != null && (! m.isEmpty()) && m.keySet().iterator().next() instanceof Comparable) return new TreeMap<>(m); return m; } /** * Sorts the specified collection if {@link SerializerSession#isSortCollections()} returns true. * * @param c The collection being sorted. * @return A new sorted {@link TreeSet}. */ public final Collection sort(Collection c) { if (isSortCollections() && c != null && (! c.isEmpty()) && c.iterator().next() instanceof Comparable) return new TreeSet<>(c); return c; } /** * Converts the contents of the specified object array to a list. * *

* Works on both object and primitive arrays. * *

* In the case of multi-dimensional arrays, the outgoing list will contain elements of type n-1 dimension. * i.e. if {@code type} is int[][] then {@code list} will have entries of type * int[]. * * @param type The type of array. * @param array The array being converted. * @return The array as a list. */ protected static final List toList(Class type, Object array) { Class componentType = type.getComponentType(); if (componentType.isPrimitive()) { int l = Array.getLength(array); List list = new ArrayList<>(l); for (int i = 0; i < l; i++) list.add(Array.get(array, i)); return list; } return Arrays.asList((Object[])array); } /** * Converts a String to an absolute URI based on the {@link UriContext} on this session. * * @param uri * The input URI. * Can be any of the following: *
    *
  • {@link java.net.URI} *
  • {@link java.net.URL} *
  • {@link CharSequence} *
* URI can be any of the following forms: *
    *
  • "foo://foo" - Absolute URI. *
  • "/foo" - Root-relative URI. *
  • "/" - Root URI. *
  • "context:/foo" - Context-root-relative URI. *
  • "context:/" - Context-root URI. *
  • "servlet:/foo" - Servlet-path-relative URI. *
  • "servlet:/" - Servlet-path URI. *
  • "request:/foo" - Request-path-relative URI. *
  • "request:/" - Request-path URI. *
  • "foo" - Path-info-relative URI. *
  • "" - Path-info URI. *
* @return The resolved URI. */ public final String resolveUri(Object uri) { return uriResolver.resolve(uri); } /** * Opposite of {@link #resolveUri(Object)}. * *

* Converts the URI to a value relative to the specified relativeTo parameter. * *

* Both parameters can be any of the following: *

    *
  • {@link java.net.URI} *
  • {@link java.net.URL} *
  • {@link CharSequence} *
* *

* Both URIs can be any of the following forms: *

    *
  • "foo://foo" - Absolute URI. *
  • "/foo" - Root-relative URI. *
  • "/" - Root URI. *
  • "context:/foo" - Context-root-relative URI. *
  • "context:/" - Context-root URI. *
  • "servlet:/foo" - Servlet-path-relative URI. *
  • "servlet:/" - Servlet-path URI. *
  • "request:/foo" - Request-path-relative URI. *
  • "request:/" - Request-path URI. *
  • "foo" - Path-info-relative URI. *
  • "" - Path-info URI. *
* * @param relativeTo The URI to relativize against. * @param uri The URI to relativize. * @return The relativized URI. */ protected final String relativizeUri(Object relativeTo, Object uri) { return uriResolver.relativize(relativeTo, uri); } /** * Converts the specified object to a String. * *

* Also has the following effects: *

    *
  • Class object is converted to a readable name. See {@link ClassUtils#getReadableClassName(Class)}. *
  • Whitespace is trimmed if the trim-strings setting is enabled. *
* * @param o The object to convert to a String. * @return The object converted to a String, or null if the input was null. */ public final String toString(Object o) { if (o == null) return null; if (o.getClass() == Class.class) return getReadableClassName((Class)o); if (o.getClass().isEnum()) return getClassMetaForObject(o).toString(o); String s = o.toString(); if (isTrimStrings()) s = s.trim(); return s; } /** * Create a "_type" property that contains the dictionary name of the bean. * * @param m The bean map to create a class property on. * @param typeName The type name of the bean. * @return A new bean property value. */ protected static final BeanPropertyValue createBeanTypeNameProperty(BeanMap m, String typeName) { BeanMeta bm = m.getMeta(); return new BeanPropertyValue(bm.getTypeProperty(), bm.getTypeProperty().getName(), typeName, null); } /** * Resolves the dictionary name for the actual type. * * @param eType The expected type of the bean property. * @param aType The actual type of the bean property. * @param pMeta The current bean property being serialized. * @return The bean dictionary name, or null if a name could not be found. */ protected final String getBeanTypeName(ClassMeta eType, ClassMeta aType, BeanPropertyMeta pMeta) { if (eType == aType) return null; if (! isAddBeanTypes()) return null; String eTypeTn = eType.getDictionaryName(); // First see if it's defined on the actual type. String tn = aType.getDictionaryName(); if (tn != null && ! tn.equals(eTypeTn)) { return tn; } // Then see if it's defined on the expected type. // The expected type might be an interface with mappings for implementation classes. BeanRegistry br = eType.getBeanRegistry(); if (br != null) { tn = br.getTypeName(aType); if (tn != null && ! tn.equals(eTypeTn)) return tn; } // Then look on the bean property. br = pMeta == null ? null : pMeta.getBeanRegistry(); if (br != null) { tn = br.getTypeName(aType); if (tn != null && ! tn.equals(eTypeTn)) return tn; } // Finally look in the session. br = getBeanRegistry(); if (br != null) { tn = br.getTypeName(aType); if (tn != null && ! tn.equals(eTypeTn)) return tn; } return null; } /** * Returns the parser-side expected type for the object. * *

* The return value depends on the {@link Serializer#SERIALIZER_addRootType} setting. * When disabled, the parser already knows the Java POJO type being parsed, so there is * no reason to add "_type" attributes to the root-level object. * * @param o The object to get the expected type on. * @return The expected type. */ protected final ClassMeta getExpectedRootType(Object o) { return isAddRootType() ? object() : getClassMetaForObject(o); } /** * Optional method that specifies HTTP request headers for this serializer. * *

* For example, {@link SoapXmlSerializer} needs to set a SOAPAction header. * *

* This method is typically meaningless if the serializer is being used stand-alone (i.e. outside of a REST server * or client). * * @return * The HTTP headers to set on HTTP requests. * Never null. */ public Map getResponseHeaders() { return Collections.emptyMap(); } /** * Returns the listener associated with this session. * * @return The listener associated with this session, or null if there is no listener. */ public SerializerListener getListener() { return listener; } /** * Returns the listener associated with this session. * * @param c The listener class to cast to. * @return The listener associated with this session, or null if there is no listener. */ @SuppressWarnings("unchecked") public T getListener(Class c) { return (T)listener; } //----------------------------------------------------------------------------------------------------------------- // Properties //----------------------------------------------------------------------------------------------------------------- /** * Configuration property: Use whitespace. * * @see Serializer#SERIALIZER_useWhitespace * @return * true if whitespace is added to the output to improve readability. */ public final boolean isUseWhitespace() { return useWhitespace; } /** * Configuration property: Add "_type" properties when needed. * * @see Serializer#SERIALIZER_addBeanTypes * @return * true if "_type" properties added to beans if their type cannot be inferred * through reflection. */ protected boolean isAddBeanTypes() { return ctx.isAddBeanTypes(); } /** * Configuration property: Trim null bean property values. * * @see Serializer#SERIALIZER_trimNullProperties * @return * true if null bean values are not serialized to the output. */ protected final boolean isTrimNullProperties() { return ctx.isTrimNullProperties(); } /** * Configuration property: Trim empty lists and arrays. * * @see Serializer#SERIALIZER_trimEmptyCollections * @return * true if empty lists and arrays are not serialized to the output. */ protected final boolean isTrimEmptyCollections() { return ctx.isTrimEmptyCollections(); } /** * Configuration property: Trim empty maps. * * @see Serializer#SERIALIZER_trimEmptyMaps * @return * true if empty map values are not serialized to the output. */ protected final boolean isTrimEmptyMaps() { return ctx.isTrimEmptyMaps(); } /** * Configuration property: Trim strings. * * @see Serializer#SERIALIZER_trimStrings * @return * true if string values will be trimmed of whitespace using {@link String#trim()} before being serialized. */ protected boolean isTrimStrings() { return ctx.isTrimStrings(); } /** * Configuration property: Sort arrays and collections alphabetically. * * @see Serializer#SERIALIZER_sortCollections * @return * true if arrays and collections are copied and sorted before serialization. */ protected final boolean isSortCollections() { return ctx.isSortCollections(); } /** * Configuration property: Sort maps alphabetically. * * @see Serializer#SERIALIZER_sortMaps * @return * true if maps are copied and sorted before serialization. */ protected final boolean isSortMaps() { return ctx.isSortMaps(); } /** * Configuration property: Add type attribute to root nodes. * * @see Serializer#SERIALIZER_addRootType * @return * true if type property should be added to root node. */ protected final boolean isAddRootType() { return ctx.isAddRootType(); } /** * Configuration property: URI context bean. * * @see Serializer#SERIALIZER_uriContext * @return * Bean used for resolution of URIs to absolute or root-relative form. */ protected final UriContext getUriContext() { return getUriContext(); } /** * Configuration property: URI resolution. * * @see Serializer#SERIALIZER_uriResolution * @return * Defines the resolution level for URIs when serializing URIs. */ protected final UriResolution getUriResolution() { return ctx.getUriResolution(); } /** * Configuration property: URI relativity. * * @see Serializer#SERIALIZER_uriRelativity * @return * Defines what relative URIs are relative to when serializing any of the following: */ protected final UriRelativity getUriRelativity() { return ctx.getUriRelativity(); } }