http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/e6bf97a8/juneau-core/src/main/java/org/apache/juneau/plaintext/package.html ---------------------------------------------------------------------- diff --git a/juneau-core/src/main/java/org/apache/juneau/plaintext/package.html b/juneau-core/src/main/java/org/apache/juneau/plaintext/package.html new file mode 100644 index 0000000..e53f25a --- /dev/null +++ b/juneau-core/src/main/java/org/apache/juneau/plaintext/package.html @@ -0,0 +1,41 @@ +<!DOCTYPE HTML> +<!-- +/*************************************************************************************************************************** + * 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. + * + ***************************************************************************************************************************/ + --> +<html> +<head> + <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> + <style type="text/css"> + /* For viewing in Page Designer */ + @IMPORT url("../../../../../../javadoc.css"); + + /* For viewing in REST interface */ + @IMPORT url("../htdocs/javadoc.css"); + body { + margin: 20px; + } + </style> + <script> + /* Replace all @code and @link tags. */ + window.onload = function() { + document.body.innerHTML = document.body.innerHTML.replace(/\{\@code ([^\}]+)\}/g, '<code>$1</code>'); + document.body.innerHTML = document.body.innerHTML.replace(/\{\@link (([^\}]+)\.)?([^\.\}]+)\}/g, '<code>$3</code>'); + } + </script> +</head> +<body> +<p>Plain-text serialization and parsing support</p> +</body> +</html> \ No newline at end of file
http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/e6bf97a8/juneau-core/src/main/java/org/apache/juneau/serializer/OutputStreamSerializer.java ---------------------------------------------------------------------- diff --git a/juneau-core/src/main/java/org/apache/juneau/serializer/OutputStreamSerializer.java b/juneau-core/src/main/java/org/apache/juneau/serializer/OutputStreamSerializer.java new file mode 100644 index 0000000..b829f77 --- /dev/null +++ b/juneau-core/src/main/java/org/apache/juneau/serializer/OutputStreamSerializer.java @@ -0,0 +1,65 @@ +/*************************************************************************************************************************** + * 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 java.io.*; + +import org.apache.juneau.annotation.*; + +/** + * Subclass of {@link Serializer} for byte-based serializers. + * + * + * <h6 class='topic'>Description</h6> + * <p> + * This class is typically the parent class of all byte-based serializers. + * It has 1 abstract method to implement... + * <ul> + * <li>{@link #doSerialize(SerializerSession, Object)} + * </ul> + * + * + * <h6 class='topic'>@Produces annotation</h6> + * <p> + * The media types that this serializer can produce is specified through the {@link Produces @Produces} annotation. + * <p> + * However, the media types can also be specified programmatically by overriding the {@link #getMediaTypes()} + * and {@link #getResponseContentType()} methods. + * + * @author James Bognar ([email protected]) + */ +public abstract class OutputStreamSerializer extends Serializer { + + @Override /* Serializer */ + public boolean isWriterSerializer() { + return false; + } + + //-------------------------------------------------------------------------------- + // Other methods + //-------------------------------------------------------------------------------- + + /** + * Convenience method for serializing an object to a <code><jk>byte</jk></code>. + * + * @param o The object to serialize. + * @return The output serialized to a string. + * @throws SerializeException If a problem occurred trying to convert the output. + */ + @Override + public final byte[] serialize(Object o) throws SerializeException { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + serialize(createSession(baos), o); + return baos.toByteArray(); + } +} http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/e6bf97a8/juneau-core/src/main/java/org/apache/juneau/serializer/SerializeException.java ---------------------------------------------------------------------- diff --git a/juneau-core/src/main/java/org/apache/juneau/serializer/SerializeException.java b/juneau-core/src/main/java/org/apache/juneau/serializer/SerializeException.java new file mode 100644 index 0000000..a43b95a --- /dev/null +++ b/juneau-core/src/main/java/org/apache/juneau/serializer/SerializeException.java @@ -0,0 +1,105 @@ +/*************************************************************************************************************************** + * 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 java.text.*; +import java.util.*; + +import org.apache.juneau.*; +import org.apache.juneau.json.*; + +/** + * General exception thrown whenever an error occurs during serialization. + * + * @author James Bognar ([email protected]) + */ +public final class SerializeException extends FormattedException { + + private static final long serialVersionUID = 1L; + + /** + * Constructor. + * + * @param session The serializer session to extract information from. + * @param message The exception message containing {@link MessageFormat}-style arguments. + * @param args Message arguments. + */ + public SerializeException(SerializerSession session, String message, Object...args) { + super(getMessage(session, message, args)); + } + + /** + * Constructor. + * + * @param message The exception message containing {@link MessageFormat}-style arguments. + * @param args Message arguments. + */ + public SerializeException(String message, Object...args) { + super(getMessage(null, message, args)); + } + + /** + * Constructor. + * + * @param session The serializer session to extract information from. + * @param causedBy The inner exception. + */ + public SerializeException(SerializerSession session, Exception causedBy) { + super(causedBy, getMessage(session, causedBy.getMessage())); + } + + /** + * Constructor. + * + * @param causedBy The inner exception. + */ + public SerializeException(Exception causedBy) { + super(causedBy, getMessage(null, causedBy.getMessage())); + } + + private static String getMessage(SerializerSession session, String msg, Object... args) { + if (args.length != 0) + msg = MessageFormat.format(msg, args); + if (session != null) { + Map<String,Object> m = session.getLastLocation(); + if (m != null && ! m.isEmpty()) + msg = "Serialize exception occurred at " + JsonSerializer.DEFAULT_LAX.toString(m) + ". " + msg; + } + return msg; + } + + /** + * Returns the highest-level <code>ParseException</code> in the stack trace. + * Useful for JUnit testing of error conditions. + * + * @return The root parse exception, or this exception if there isn't one. + */ + public SerializeException getRootCause() { + SerializeException t = this; + while (! (t.getCause() == null || ! (t.getCause() instanceof SerializeException))) + t = (SerializeException)t.getCause(); + return t; + } + + /** + * Sets the inner cause for this exception. + * + * @param cause The inner cause. + * @return This object (for method chaining). + */ + @Override /* Throwable */ + public synchronized SerializeException initCause(Throwable cause) { + super.initCause(cause); + return this; + } +} \ No newline at end of file http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/e6bf97a8/juneau-core/src/main/java/org/apache/juneau/serializer/Serializer.java ---------------------------------------------------------------------- diff --git a/juneau-core/src/main/java/org/apache/juneau/serializer/Serializer.java b/juneau-core/src/main/java/org/apache/juneau/serializer/Serializer.java new file mode 100644 index 0000000..c730705 --- /dev/null +++ b/juneau-core/src/main/java/org/apache/juneau/serializer/Serializer.java @@ -0,0 +1,335 @@ +/*************************************************************************************************************************** + * 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 java.io.*; +import java.lang.reflect.*; +import java.text.*; +import java.util.*; + +import org.apache.juneau.*; +import org.apache.juneau.annotation.*; +import org.apache.juneau.internal.*; +import org.apache.juneau.soap.*; + +/** + * Parent class for all Juneau serializers. + * + * + * <h6 class='topic'>Description</h6> + * <p> + * Base serializer class that serves as the parent class for all serializers. + * <p> + * Subclasses should extend directly from {@link OutputStreamSerializer} or {@link WriterSerializer}. + * + * + * <h6 class='topic'>@Produces annotation</h6> + * <p> + * The media types that this serializer can produce is specified through the {@link Produces @Produces} annotation. + * <p> + * However, the media types can also be specified programmatically by overriding the {@link #getMediaTypes()} + * and {@link #getResponseContentType()} methods. + * + * + * <h6 class='topic'>Configurable properties</h6> + * See {@link SerializerContext} for a list of configurable properties that can be set on this class + * using the {@link #setProperty(String, Object)} method. + * + * @author James Bognar ([email protected]) + */ +public abstract class Serializer extends CoreApi { + + private final String[] mediaTypes; + private final MediaRange[] mediaRanges; + private final String contentType; + + // Hidden constructors to force subclass from OuputStreamSerializer or WriterSerializer. + Serializer() { + Produces p = ReflectionUtils.getAnnotation(Produces.class, getClass()); + if (p == null) + throw new RuntimeException(MessageFormat.format("Class ''{0}'' is missing the @Produces annotation", getClass().getName())); + this.mediaTypes = p.value(); + for (int i = 0; i < mediaTypes.length; i++) { + mediaTypes[i] = mediaTypes[i].toLowerCase(Locale.ENGLISH); + } + + List<MediaRange> l = new LinkedList<MediaRange>(); + for (int i = 0; i < mediaTypes.length; i++) + l.addAll(Arrays.asList(MediaRange.parse(mediaTypes[i]))); + mediaRanges = l.toArray(new MediaRange[l.size()]); + + String ct = p.contentType().isEmpty() ? p.value()[0] : p.contentType(); + contentType = ct.isEmpty() ? null : ct; + } + + /** + * Returns <jk>true</jk> if this parser subclasses from {@link WriterSerializer}. + * + * @return <jk>true</jk> if this parser subclasses from {@link WriterSerializer}. + */ + public abstract boolean isWriterSerializer(); + + //-------------------------------------------------------------------------------- + // Abstract methods + //-------------------------------------------------------------------------------- + + /** + * Serializes a POJO to the specified output stream or writer. + * <p> + * This method should NOT close the context object. + * @param session The serializer session object return by {@link #createSession(Object, ObjectMap, Method)}.<br> + * If <jk>null</jk>, session is created using {@link #createSession(Object)}. + * @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(SerializerSession session, Object o) throws Exception; + + /** + * Shortcut method for serializing objects directly to either a <code>String</code> or <code><jk>byte</jk>[]</code> + * depending on the serializer type. + * <p> + * + * @param o The object to serialize. + * @return The serialized object. + * <br>Character-based serializers will return a <code>String</code> + * <br>Stream-based serializers will return a <code><jk>byte</jk>[]</code> + * @throws SerializeException If a problem occurred trying to convert the output. + */ + public abstract Object serialize(Object o) throws SerializeException; + + //-------------------------------------------------------------------------------- + // Other methods + //-------------------------------------------------------------------------------- + + /** + * Serialize the specified object using the specified session. + * + * @param session The serializer session object return by {@link #createSession(Object, ObjectMap, Method)}.<br> + * If <jk>null</jk>, session is created using {@link #createSession(Object)}. + * @param o The object to serialize. + * @throws SerializeException If a problem occurred trying to convert the output. + */ + public final void serialize(SerializerSession session, Object o) throws SerializeException { + try { + doSerialize(session, o); + } catch (SerializeException e) { + throw e; + } catch (StackOverflowError e) { + throw new SerializeException(session, "Stack overflow occurred. This can occur when trying to serialize models containing loops. It's recommended you use the SerializerContext.SERIALIZER_detectRecursions setting to help locate the loop.").initCause(e); + } catch (Exception e) { + throw new SerializeException(session, e); + } finally { + session.close(); + } + } + + /** + * Serializes a POJO to the specified output stream or writer. + * <p> + * Equivalent to calling <code>serializer.serialize(o, out, <jk>null</jk>);</code> + * + * @param o The object to serialize. + * @param output The output object. + * <br>Character-based serializers can handle the following output class types: + * <ul> + * <li>{@link Writer} + * <li>{@link OutputStream} - Output will be written as UTF-8 encoded stream. + * <li>{@link File} - Output will be written as system-default encoded stream. + * </ul> + * <br>Stream-based serializers can handle the following output class types: + * <ul> + * <li>{@link OutputStream} + * <li>{@link File} + * </ul> + * @throws SerializeException If a problem occurred trying to convert the output. + */ + public final void serialize(Object o, Object output) throws SerializeException { + SerializerSession session = createSession(output); + serialize(session, o); + } + + /** + * Create the session object that will be passed in to the serialize method. + * <p> + * It's up to implementers to decide what the session object looks like, although typically + * it's going to be a subclass of {@link SerializerSession}. + * + * @param output The output object. + * <br>Character-based serializers can handle the following output class types: + * <ul> + * <li>{@link Writer} + * <li>{@link OutputStream} - Output will be written as UTF-8 encoded stream. + * <li>{@link File} - Output will be written as system-default encoded stream. + * </ul> + * <br>Stream-based serializers can handle the following output class types: + * <ul> + * <li>{@link OutputStream} + * <li>{@link File} + * </ul> + * @param properties Optional additional properties. + * @param javaMethod 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 new session. + */ + public SerializerSession createSession(Object output, ObjectMap properties, Method javaMethod) { + return new SerializerSession(getContext(SerializerContext.class), getBeanContext(), output, properties, javaMethod); + } + + /** + * Create a basic session object without overriding properties or specifying <code>javaMethod</code>. + * <p> + * Equivalent to calling <code>createSession(<jk>null</jk>, <jk>null</jk>)</code>. + * + * @param output The output object. + * <br>Character-based serializers can handle the following output class types: + * <ul> + * <li>{@link Writer} + * <li>{@link OutputStream} - Output will be written as UTF-8 encoded stream. + * <li>{@link File} - Output will be written as system-default encoded stream. + * </ul> + * <br>Stream-based serializers can handle the following output class types: + * <ul> + * <li>{@link OutputStream} + * <li>{@link File} + * </ul> + * @return The new session. + */ + protected SerializerSession createSession(Object output) { + return createSession(output, null, null); + } + + /** + * Converts the contents of the specified object array to a list. + * <p> + * Works on both object and primitive arrays. + * <p> + * In the case of multi-dimensional arrays, the outgoing list will + * contain elements of type n-1 dimension. i.e. if {@code type} is <code><jk>int</jk>[][]</code> + * then {@code list} will have entries of type <code><jk>int</jk>[]</code>. + * + * @param type The type of array. + * @param array The array being converted. + * @return The array as a list. + */ + protected final List<Object> toList(Class<?> type, Object array) { + Class<?> componentType = type.getComponentType(); + if (componentType.isPrimitive()) { + int l = Array.getLength(array); + List<Object> list = new ArrayList<Object>(l); + for (int i = 0; i < l; i++) + list.add(Array.get(array, i)); + return list; + } + return Arrays.asList((Object[])array); + } + + /** + * Returns the media types handled based on the value of the {@link Produces} annotation on the serializer class. + * <p> + * This method can be overridden by subclasses to determine the media types programatically. + * + * @return The list of media types. Never <jk>null</jk>. + */ + public String[] getMediaTypes() { + return mediaTypes; + } + + /** + * Returns the results from {@link #getMediaTypes()} parsed as {@link MediaRange MediaRanges}. + * + * @return The list of media types parsed as ranges. Never <jk>null</jk>. + */ + public MediaRange[] getMediaRanges() { + return mediaRanges; + } + + /** + * Optional method that specifies HTTP request headers for this serializer. + * <p> + * For example, {@link SoapXmlSerializer} needs to set a <code>SOAPAction</code> header. + * <p> + * This method is typically meaningless if the serializer is being used standalone (i.e. outside of a REST server or client). + * + * @param properties Optional run-time properties (the same that are passed to {@link WriterSerializer#doSerialize(SerializerSession, Object)}. + * Can be <jk>null</jk>. + * @return The HTTP headers to set on HTTP requests. + * Can be <jk>null</jk>. + */ + public ObjectMap getResponseHeaders(ObjectMap properties) { + return new ObjectMap(getBeanContext()); + } + + /** + * Optional method that returns the response <code>Content-Type</code> for this serializer if it is different from the matched media type. + * <p> + * This method is specified to override the content type for this serializer. + * For example, the {@link org.apache.juneau.json.JsonSerializer.Simple} class returns that it handles media type <js>"text/json+simple"</js>, but returns + * <js>"text/json"</js> as the actual content type. + * This allows clients to request specific 'flavors' of content using specialized <code>Accept</code> header values. + * <p> + * This method is typically meaningless if the serializer is being used standalone (i.e. outside of a REST server or client). + * + * @return The response content type. If <jk>null</jk>, then the matched media type is used. + */ + public String getResponseContentType() { + return contentType; + } + + //-------------------------------------------------------------------------------- + // Overridden methods + //-------------------------------------------------------------------------------- + + @Override /* CoreApi */ + public Serializer setProperty(String property, Object value) throws LockedException { + super.setProperty(property, value); + return this; + } + + @Override /* CoreApi */ + public Serializer addNotBeanClasses(Class<?>...classes) throws LockedException { + super.addNotBeanClasses(classes); + return this; + } + + @Override /* CoreApi */ + public Serializer addTransforms(Class<?>...classes) throws LockedException { + super.addTransforms(classes); + return this; + } + + @Override /* CoreApi */ + public <T> Serializer addImplClass(Class<T> interfaceClass, Class<? extends T> implClass) throws LockedException { + super.addImplClass(interfaceClass, implClass); + return this; + } + + @Override /* CoreApi */ + public Serializer setClassLoader(ClassLoader classLoader) throws LockedException { + super.setClassLoader(classLoader); + return this; + } + + @Override /* CoreApi */ + public Serializer lock() { + super.lock(); + return this; + } + + @Override /* CoreApi */ + public Serializer clone() throws CloneNotSupportedException { + Serializer c = (Serializer)super.clone(); + return c; + } +} http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/e6bf97a8/juneau-core/src/main/java/org/apache/juneau/serializer/SerializerContext.java ---------------------------------------------------------------------- diff --git a/juneau-core/src/main/java/org/apache/juneau/serializer/SerializerContext.java b/juneau-core/src/main/java/org/apache/juneau/serializer/SerializerContext.java new file mode 100644 index 0000000..bc4ed6b --- /dev/null +++ b/juneau-core/src/main/java/org/apache/juneau/serializer/SerializerContext.java @@ -0,0 +1,291 @@ +/*************************************************************************************************************************** + * 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 org.apache.juneau.*; +import org.apache.juneau.internal.*; + +/** + * Parent class for all serializer contexts. + * + * @author James Bognar ([email protected]) + */ +public class SerializerContext extends Context { + + /** + * Max serialization depth ({@link Integer}, default=<code>100</code>). + * <p> + * Abort serialization if specified depth is reached in the POJO tree. + * If this depth is exceeded, an exception is thrown. + * This prevents stack overflows from occurring when trying to serialize models with recursive references. + */ + public static final String SERIALIZER_maxDepth = "Serializer.maxDepth"; + + /** + * Initial depth ({@link Integer}, default=<code>0</code>). + * <p> + * The initial indentation level at the root. + * Useful when constructing document fragments that need to be indented at a certain level. + */ + public static final String SERIALIZER_initialDepth = "Serializer.initialDepth"; + + /** + * Automatically detect POJO recursions ({@link Boolean}, default=<jk>false</jk>). + * <p> + * Specifies that recursions should be checked for during serialization. + * <p> + * Recursions can occur when serializing models that aren't true trees, but rather contain loops. + * <p> + * The behavior when recursions are detected depends on the value for {@link #SERIALIZER_ignoreRecursions}. + * <p> + * For example, if a model contains the links A->B->C->A, then the JSON generated will look like + * the following when <jsf>SERIALIZER_ignoreRecursions</jsf> is <jk>true</jk>... + * <code>{A:{B:{C:null}}}</code><br> + * <p> + * Note: Checking for recursion can cause a small performance penalty. + */ + public static final String SERIALIZER_detectRecursions = "Serializer.detectRecursions"; + + /** + * Ignore recursion errors ({@link Boolean}, default=<jk>false</jk>). + * <p> + * Used in conjunction with {@link #SERIALIZER_detectRecursions}. + * Setting is ignored if <jsf>SERIALIZER_detectRecursions</jsf> is <jk>false</jk>. + * <p> + * If <jk>true</jk>, when we encounter the same object when serializing a tree, + * we set the value to <jk>null</jk>. + * Otherwise, an exception is thrown. + */ + public static final String SERIALIZER_ignoreRecursions = "Serializer.ignoreRecursions"; + + /** + * Debug mode ({@link Boolean}, default=<jk>false</jk>). + * <p> + * Enables the following additional information during serialization: + * <ul class='spaced-list'> + * <li>When bean getters throws exceptions, the exception includes the object stack information + * in order to determine how that method was invoked. + * <li>Enables {@link #SERIALIZER_detectRecursions}. + * </ul> + */ + public static final String SERIALIZER_debug = "Serializer.debug"; + + /** + * Use indentation in output ({@link Boolean}, default=<jk>false</jk>). + * <p> + * If <jk>true</jk>, newlines and indentation is added to the output to improve readability. + */ + public static final String SERIALIZER_useIndentation = "Serializer.useIndentation"; + + /** + * Add class attributes to output ({@link Boolean}, default=<jk>false</jk>). + * <p> + * If <jk>true</jk>, then <js>"_class"</js> attributes will be added to beans if their type cannot be inferred through reflection. + * This is used to recreate the correct objects during parsing if the object types cannot be inferred. + * For example, when serializing a {@code Map<String,Object>} field, where the bean class cannot be determined from the value type. + */ + public static final String SERIALIZER_addClassAttrs = "Serializer.addClassAttrs"; + + /** + * Quote character ({@link Character}, default=<js>'"'</js>). + * <p> + * This is the character used for quoting attributes and values. + */ + public static final String SERIALIZER_quoteChar = "Serializer.quoteChar"; + + /** + * Trim null bean property values from output ({@link Boolean}, default=<jk>true</jk>). + * <p> + * If <jk>true</jk>, null bean values will not be serialized to the output. + * <p> + * Note that enabling this setting has the following effects on parsing: + * <ul class='spaced-list'> + * <li>Map entries with <jk>null</jk> values will be lost. + * </ul> + */ + public static final String SERIALIZER_trimNullProperties = "Serializer.trimNullProperties"; + + /** + * Trim empty lists and arrays from output ({@link Boolean}, default=<jk>false</jk>). + * <p> + * If <jk>true</jk>, empty list values will not be serialized to the output. + * <p> + * Note that enabling this setting has the following effects on parsing: + * <ul class='spaced-list'> + * <li>Map entries with empty list values will be lost. + * <li>Bean properties with empty list values will not be set. + * </ul> + */ + public static final String SERIALIZER_trimEmptyLists = "Serializer.trimEmptyLists"; + + /** + * Trim empty maps from output ({@link Boolean}, default=<jk>false</jk>). + * <p> + * If <jk>true</jk>, empty map values will not be serialized to the output. + * <p> + * Note that enabling this setting has the following effects on parsing: + * <ul class='spaced-list'> + * <li>Bean properties with empty map values will not be set. + * </ul> + */ + public static final String SERIALIZER_trimEmptyMaps = "Serializer.trimEmptyMaps"; + + /** + * Trim strings in output ({@link Boolean}, default=<jk>false</jk>). + * <p> + * If <jk>true</jk>, string values will be trimmed of whitespace using {@link String#trim()} before being serialized. + */ + public static final String SERIALIZER_trimStrings = "Serializer.trimStrings"; + + /** + * URI base for relative URIs ({@link String}, default=<js>""</js>). + * <p> + * Prepended to relative URIs during serialization (along with the {@link #SERIALIZER_absolutePathUriBase} if specified. + * (i.e. URIs not containing a schema and not starting with <js>'/'</js>). + * (e.g. <js>"foo/bar"</js>) + * + * <dl> + * <dt>Examples:</dt> + * <dd> + * <table class='styled'> + * <tr><th>SERIALIZER_relativeUriBase</th><th>URI</th><th>Serialized URI</th></tr> + * <tr> + * <td><code>http://foo:9080/bar/baz</code></td> + * <td><code>mywebapp</code></td> + * <td><code>http://foo:9080/bar/baz/mywebapp</code></td> + * </tr> + * <tr> + * <td><code>http://foo:9080/bar/baz</code></td> + * <td><code>/mywebapp</code></td> + * <td><code>/mywebapp</code></td> + * </tr> + * <tr> + * <td><code>http://foo:9080/bar/baz</code></td> + * <td><code>http://mywebapp</code></td> + * <td><code>http://mywebapp</code></td> + * </tr> + * </table> + * </dd> + * </dl> + */ + public static final String SERIALIZER_relativeUriBase = "Serializer.relativeUriBase"; + + /** + * Sort arrays and collections alphabetically before serializing ({@link Boolean}, default=<jk>false</jk>). + * <p> + * Note that this introduces a performance penalty. + */ + public static final String SERIALIZER_sortCollections = "Serializer.sortCollections"; + + /** + * Sort maps alphabetically before serializing ({@link Boolean}, default=<jk>false</jk>). + * <p> + * Note that this introduces a performance penalty. + */ + public static final String SERIALIZER_sortMaps = "Serializer.sortMaps"; + + /** + * URI base for relative URIs with absolute paths ({@link String}, default=<js>""</js>). + * <p> + * Prepended to relative absolute-path URIs during serialization. + * (i.e. URIs starting with <js>'/'</js>). + * (e.g. <js>"/foo/bar"</js>) + * + * <dl> + * <dt>Examples:</dt> + * <dd> + * <table class='styled'> + * <tr><th>SERIALIZER_absolutePathUriBase</th><th>URI</th><th>Serialized URI</th></tr> + * <tr> + * <td><code>http://foo:9080/bar/baz</code></td> + * <td><code>mywebapp</code></td> + * <td><code>mywebapp</code></td> + * </tr> + * <tr> + * <td><code>http://foo:9080/bar/baz</code></td> + * <td><code>/mywebapp</code></td> + * <td><code>http://foo:9080/bar/baz/mywebapp</code></td> + * </tr> + * <tr> + * <td><code>http://foo:9080/bar/baz</code></td> + * <td><code>http://mywebapp</code></td> + * <td><code>http://mywebapp</code></td> + * </tr> + * </table> + * </dd> + * </dl> + */ + public static final String SERIALIZER_absolutePathUriBase = "Serializer.absolutePathUriBase"; + + + final int maxDepth, initialDepth; + final boolean + debug, + detectRecursions, + ignoreRecursions, + useIndentation, + addClassAttrs, + trimNulls, + trimEmptyLists, + trimEmptyMaps, + trimStrings, + sortCollections, + sortMaps; + final char quoteChar; + final String relativeUriBase, absolutePathUriBase; + + /** + * Constructor. + * <p> + * Typically only called from {@link ContextFactory#getContext(Class)}. + * + * @param cf The factory that created this context. + */ + public SerializerContext(ContextFactory cf) { + super(cf); + maxDepth = cf.getProperty(SERIALIZER_maxDepth, int.class, 100); + initialDepth = cf.getProperty(SERIALIZER_initialDepth, int.class, 0); + debug = cf.getProperty(SERIALIZER_debug, boolean.class, false); + detectRecursions = cf.getProperty(SERIALIZER_detectRecursions, boolean.class, false); + ignoreRecursions = cf.getProperty(SERIALIZER_ignoreRecursions, boolean.class, false); + useIndentation = cf.getProperty(SERIALIZER_useIndentation, boolean.class, false); + addClassAttrs = cf.getProperty(SERIALIZER_addClassAttrs, boolean.class, false); + trimNulls = cf.getProperty(SERIALIZER_trimNullProperties, boolean.class, true); + trimEmptyLists = cf.getProperty(SERIALIZER_trimEmptyLists, boolean.class, false); + trimEmptyMaps = cf.getProperty(SERIALIZER_trimEmptyMaps, boolean.class, false); + trimStrings = cf.getProperty(SERIALIZER_trimStrings, boolean.class, false); + sortCollections = cf.getProperty(SERIALIZER_sortCollections, boolean.class, false); + sortMaps = cf.getProperty(SERIALIZER_sortMaps, boolean.class, false); + quoteChar = cf.getProperty(SERIALIZER_quoteChar, String.class, "\"").charAt(0); + relativeUriBase = resolveRelativeUriBase(cf.getProperty(SERIALIZER_relativeUriBase, String.class, "")); + absolutePathUriBase = resolveAbsolutePathUriBase(cf.getProperty(SERIALIZER_absolutePathUriBase, String.class, "")); + } + + private String resolveRelativeUriBase(String s) { + if (StringUtils.isEmpty(s)) + return null; + if (s.equals("/")) + return s; + else if (StringUtils.endsWith(s, '/')) + s = s.substring(0, s.length()-1); + return s; + } + + private String resolveAbsolutePathUriBase(String s) { + if (StringUtils.isEmpty(s)) + return null; + if (StringUtils.endsWith(s, '/')) + s = s.substring(0, s.length()-1); + return s; + } +} http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/e6bf97a8/juneau-core/src/main/java/org/apache/juneau/serializer/SerializerGroup.java ---------------------------------------------------------------------- diff --git a/juneau-core/src/main/java/org/apache/juneau/serializer/SerializerGroup.java b/juneau-core/src/main/java/org/apache/juneau/serializer/SerializerGroup.java new file mode 100644 index 0000000..926075e --- /dev/null +++ b/juneau-core/src/main/java/org/apache/juneau/serializer/SerializerGroup.java @@ -0,0 +1,338 @@ +/*************************************************************************************************************************** + * 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.ArrayUtils.*; + +import java.util.*; +import java.util.concurrent.*; +import java.util.concurrent.locks.*; + +import org.apache.juneau.*; + +/** + * Represents a group of {@link Serializer Serializers} that can be looked up by media type. + * + * + * <h6 class='topic'>Description</h6> + * <p> + * Provides the following features: + * <ul class='spaced-list'> + * <li>Finds serializers based on HTTP <code>Accept</code> header values. + * <li>Sets common properties on all serializers in a single method call. + * <li>Locks all serializers in a single method call. + * <li>Clones existing groups and all serializers within the group in a single method call. + * </ul> + * + * + * <h6 class='topic'>Match ordering</h6> + * <p> + * Serializers are matched against <code>Accept</code> strings in the order they exist in this group. + * <p> + * Adding new entries will cause the entries to be prepended to the group. + * This allows for previous serializers to be overridden through subsequent calls. + * <p> + * For example, calling <code>g.append(S1.<jk>class</jk>,S2.<jk>class</jk>).append(S3.<jk>class</jk>,S4.<jk>class</jk>)</code> + * will result in the order <code>S3, S4, S1, S2</code>. + * + * + * <h6 class='topic'>Examples</h6> + * <p class='bcode'> + * <jc>// Construct a new serializer group</jc> + * SerializerGroup g = <jk>new</jk> SerializerGroup(); + * + * <jc>// Add some serializers to it</jc> + * g.append(JsonSerializer.<jk>class</jk>, XmlSerializer.<jk>class</jk>); + * + * <jc>// Change settings for all serializers in the group and lock it.</jc> + * g.setProperty(SerializerContext.<jsf>SERIALIZER_useIndentation</jsf>, <jk>true</jk>) + * .addTransforms(CalendarTransform.ISO8601DT.<jk>class</jk>) + * .lock(); + * + * <jc>// Find the appropriate serializer by Accept type</jc> + * String mediaTypeMatch = g.findMatch(<js>"text/foo, text/json;q=0.8, text/*;q:0.6, *\/*;q=0.0"</js>); + * WriterSerializer s = (WriterSerializer)g.getSerializer(mediaTypeMatch); + * + * <jc>// Serialize a bean to JSON text </jc> + * AddressBook addressBook = <jk>new</jk> AddressBook(); <jc>// Bean to serialize.</jc> + * String json = s.serialize(addressBook); + * </p> + * + * @author James Bognar ([email protected]) + */ +public final class SerializerGroup extends Lockable { + + // Maps media-types to serializers. + private final Map<String,Serializer> serializerMap = new ConcurrentHashMap<String,Serializer>(); + + // Maps Accept headers to matching media types. + private final Map<String,String> mediaTypeMappings = new ConcurrentHashMap<String,String>(); + + private final CopyOnWriteArrayList<Serializer> serializers = new CopyOnWriteArrayList<Serializer>(); + + private final ReadWriteLock lock = new ReentrantReadWriteLock(); + private final Lock rl = lock.readLock(), wl = lock.writeLock(); + + + /** + * Registers the specified serializers with this group. + * + * @param s The serializers to append to this group. + * @return This object (for method chaining). + * @throws Exception Thrown if {@link Serializer} could not be constructed. + */ + public SerializerGroup append(Class<? extends Serializer>...s) throws Exception { + checkLock(); + wl.lock(); + try { + serializerMap.clear(); + mediaTypeMappings.clear(); + for (Class<? extends Serializer> ss : reverse(s)) { + try { + append(ss); + } catch (NoClassDefFoundError e) { + // Ignore if dependent library not found (e.g. Jena). + System.err.println(e); + } + } + } finally { + wl.unlock(); + } + return this; + } + + /** + * Same as {@link #append(Class[])}, except specify a single class to avoid unchecked compile warnings. + * + * @param c The serializer to append to this group. + * @return This object (for method chaining). + * @throws Exception Thrown if {@link Serializer} could not be constructed. + */ + public SerializerGroup append(Class<? extends Serializer> c) throws Exception { + checkLock(); + wl.lock(); + try { + serializerMap.clear(); + mediaTypeMappings.clear(); + serializers.add(0, c.newInstance()); + } catch (NoClassDefFoundError e) { + // Ignore if dependent library not found (e.g. Jena). + System.err.println(e); + } finally { + wl.unlock(); + } + return this; + } + + /** + * Returns the serializer registered to handle the specified media type. + * <p> + * The media-type string must not contain any parameters or q-values. + * + * @param mediaType The media-type string (e.g. <js>"text/json"</js> + * @return The serializer that handles the specified accept content type, or <jk>null</jk> if + * no serializer is registered to handle it. + */ + public Serializer getSerializer(String mediaType) { + Serializer s = serializerMap.get(mediaType); + if (s == null) + s = serializerMap.get(findMatch(mediaType)); + return s; + } + + /** + * Searches the group for a serializer that can handle the specified <code>Accept</code> value. + * <p> + * The <code>accept</code> value complies with the syntax described in RFC2616, Section 14.1, as described below: + * <p class='bcode'> + * Accept = "Accept" ":" + * #( media-range [ accept-params ] ) + * + * media-range = ( "*\/*" + * | ( type "/" "*" ) + * | ( type "/" subtype ) + * ) *( ";" parameter ) + * accept-params = ";" "q" "=" qvalue *( accept-extension ) + * accept-extension = ";" token [ "=" ( token | quoted-string ) ] + * </p> + * <p> + * The general idea behind having the serializer resolution be a two-step process is so that + * the matched media type can be passed in to the {@link WriterSerializer#doSerialize(SerializerSession, Object)} method. + * For example... + * <p class='bcode'> + * String acceptHeaderValue = request.getHeader(<js>"Accept"</js>); + * String matchingMediaType = group.findMatch(acceptHeaderValue); + * if (matchingMediaType == <jk>null</jk>) + * <jk>throw new</jk> RestException(<jsf>SC_NOT_ACCEPTABLE</jsf>); + * WriterSerializer s = (WriterSerializer)group.getSerializer(matchingMediaType); + * s.serialize(getPojo(), response.getWriter(), response.getProperties(), matchingMediaType); + * </p> + * + * @param acceptHeader The HTTP <l>Accept</l> header string. + * @return The media type registered by one of the parsers that matches the <code>accept</code> string, + * or <jk>null</jk> if no media types matched. + */ + public String findMatch(String acceptHeader) { + rl.lock(); + try { + String mt = mediaTypeMappings.get(acceptHeader); + if (mt != null) + return mt; + + MediaRange[] mr = MediaRange.parse(acceptHeader); + if (mr.length == 0) + mr = MediaRange.parse("*/*"); + + for (MediaRange a : mr) { + for (Serializer s : serializers) { + for (MediaRange a2 : s.getMediaRanges()) { + if (a.matches(a2)) { + mt = a2.getMediaType(); + mediaTypeMappings.put(acceptHeader, mt); + serializerMap.put(mt, s); + return mt; + } + } + } + } + return null; + } finally { + rl.unlock(); + } + } + + /** + * Returns the media types that all parsers in this group can handle + * <p> + * Entries are ordered in the same order as the parsers in the group. + * + * @return The list of media types. + */ + public List<String> getSupportedMediaTypes() { + List<String> l = new ArrayList<String>(); + for (Serializer s : serializers) + for (String mt : s.getMediaTypes()) + if (! l.contains(mt)) + l.add(mt); + return l; + } + + //-------------------------------------------------------------------------------- + // Convenience methods for setting properties on all serializers. + //-------------------------------------------------------------------------------- + + /** + * Shortcut for calling {@link Serializer#setProperty(String, Object)} on all serializers in this group. + * + * @param property The property name. + * @param value The property value. + * @throws LockedException If {@link #lock()} was called on this object. + * @return This object (for method chaining). + */ + public SerializerGroup setProperty(String property, Object value) throws LockedException { + checkLock(); + for (Serializer s : serializers) + s.setProperty(property, value); + return this; + } + + /** + * Shortcut for calling {@link Serializer#setProperties(ObjectMap)} on all serializers in this group. + * + * @param properties The properties to set. Ignored if <jk>null</jk>. + * @throws LockedException If {@link #lock()} was called on this object. + * @return This object (for method chaining). + */ + public SerializerGroup setProperties(ObjectMap properties) { + checkLock(); + for (Serializer s : serializers) + s.setProperties(properties); + return this; + } + + /** + * Shortcut for calling {@link Serializer#addNotBeanClasses(Class[])} on all serializers in this group. + * + * @param classes The classes to specify as not-beans to the underlying bean context of all serializers in this group. + * @throws LockedException If {@link #lock()} was called on this object. + * @return This object (for method chaining). + */ + public SerializerGroup addNotBeanClasses(Class<?>...classes) throws LockedException { + checkLock(); + for (Serializer s : serializers) + s.addNotBeanClasses(classes); + return this; + } + + /** + * Shortcut for calling {@link Serializer#addTransforms(Class[])} on all serializers in this group. + * + * @param classes The classes to add bean transforms for to the underlying bean context of all serializers in this group. + * @throws LockedException If {@link #lock()} was called on this object. + * @return This object (for method chaining). + */ + public SerializerGroup addTransforms(Class<?>...classes) throws LockedException { + checkLock(); + for (Serializer s : serializers) + s.addTransforms(classes); + return this; + } + + /** + * Shortcut for calling {@link Serializer#addImplClass(Class, Class)} on all serializers in this group. + * + * @param <T> The interface or abstract class type. + * @param interfaceClass The interface or abstract class. + * @param implClass The implementation class. + * @throws LockedException If {@link #lock()} was called on this object. + * @return This object (for method chaining). + */ + public <T> SerializerGroup addImplClass(Class<T> interfaceClass, Class<? extends T> implClass) throws LockedException { + checkLock(); + for (Serializer s : serializers) + s.addImplClass(interfaceClass, implClass); + return this; + } + + + //-------------------------------------------------------------------------------- + // Overridden methods + //-------------------------------------------------------------------------------- + + /** + * Locks this group and all serializers in this group. + */ + @Override /* Lockable */ + public SerializerGroup lock() { + super.lock(); + for (Serializer s : serializers) + s.lock(); + return this; + } + + /** + * Clones this group and all serializers in this group. + */ + @Override /* Lockable */ + public SerializerGroup clone() throws CloneNotSupportedException { + SerializerGroup g = new SerializerGroup(); + + List<Serializer> l = new ArrayList<Serializer>(serializers.size()); + for (Serializer s : serializers) + l.add(s.clone()); + + g.serializers.addAll(l); + + return g; + } +} http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/e6bf97a8/juneau-core/src/main/java/org/apache/juneau/serializer/SerializerSession.java ---------------------------------------------------------------------- diff --git a/juneau-core/src/main/java/org/apache/juneau/serializer/SerializerSession.java b/juneau-core/src/main/java/org/apache/juneau/serializer/SerializerSession.java new file mode 100644 index 0000000..eb5dd94 --- /dev/null +++ b/juneau-core/src/main/java/org/apache/juneau/serializer/SerializerSession.java @@ -0,0 +1,743 @@ +/*************************************************************************************************************************** + * 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.serializer.SerializerContext.*; + +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.transform.*; + +/** + * Context object that lives for the duration of a single use of {@link Serializer}. + * <p> + * Used by serializers for the following purposes: + * <ul class='spaced-list'> + * <li>Keeping track of how deep it is in a model for indentation purposes. + * <li>Ensuring infinite loops don't occur by setting a limit on how deep to traverse a model. + * <li>Ensuring infinite loops don't occur from loops in the model (when detectRecursions is enabled. + * <li>Allowing serializer properties to be overridden on method calls. + * </ul> + * <p> + * This class is NOT thread safe. It is meant to be discarded after one-time use. + * + * @author James Bognar ([email protected]) + */ +public class SerializerSession extends Session { + + private static JuneauLogger logger = JuneauLogger.getLogger(SerializerSession.class); + + private final int maxDepth, initialDepth; + private final boolean + debug, + detectRecursions, + ignoreRecursions, + useIndentation, + addClassAttrs, + trimNulls, + trimEmptyLists, + trimEmptyMaps, + trimStrings, + sortCollections, + sortMaps; + private final char quoteChar; + private final String relativeUriBase, absolutePathUriBase; + + private final ObjectMap overrideProperties; + + /** The current indentation depth into the model. */ + public int indent; + + private boolean closed; + private final Map<Object,Object> set; // Contains the current objects in the current branch of the model. + private final LinkedList<StackElement> stack = new LinkedList<StackElement>(); // Contains the current objects in the current branch of the model. + private boolean isBottom; // If 'true', then we're at a leaf in the model (i.e. a String, Number, Boolean, or null). + private final List<String> warnings = new LinkedList<String>(); // Any warnings encountered. + private final BeanContext beanContext; // The bean context being used for this session. + private final Method javaMethod; // Java method that invoked this serializer. + private final Object output; + private OutputStream outputStream; + private Writer writer, flushOnlyWriter; + private BeanPropertyMeta<?> currentProperty; + private ClassMeta<?> currentClass; + + + /** + * 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. + * @param beanContext The bean context being used. + * @param output The output object. + * <br>Character-based serializers can handle the following output class types: + * <ul> + * <li>{@link Writer} + * <li>{@link OutputStream} - Output will be written as UTF-8 encoded stream. + * <li>{@link File} - Output will be written as system-default encoded stream. + * </ul> + * <br>Stream-based serializers can handle the following output class types: + * <ul> + * <li>{@link OutputStream} + * <li>{@link File} + * </ul> + * @param op The override properties. + * These override any context properties defined in the context. + * @param javaMethod The java method that called this parser, usually the method in a REST servlet. + */ + public SerializerSession(SerializerContext ctx, BeanContext beanContext, Object output, ObjectMap op, Method javaMethod) { + super(ctx); + this.beanContext = beanContext; + this.javaMethod = javaMethod; + this.output = output; + if (op == null || op.isEmpty()) { + overrideProperties = new ObjectMap(); + maxDepth = ctx.maxDepth; + initialDepth = ctx.initialDepth; + debug = ctx.debug; + detectRecursions = ctx.detectRecursions; + ignoreRecursions = ctx.ignoreRecursions; + useIndentation = ctx.useIndentation; + addClassAttrs = ctx.addClassAttrs; + trimNulls = ctx.trimNulls; + trimEmptyLists = ctx.trimEmptyLists; + trimEmptyMaps = ctx.trimEmptyMaps; + trimStrings = ctx.trimStrings; + quoteChar = ctx.quoteChar; + relativeUriBase = ctx.relativeUriBase; + absolutePathUriBase = ctx.absolutePathUriBase; + sortCollections = ctx.sortCollections; + sortMaps = ctx.sortMaps; + } else { + overrideProperties = op; + maxDepth = op.getInt(SERIALIZER_maxDepth, ctx.maxDepth); + initialDepth = op.getInt(SERIALIZER_initialDepth, ctx.initialDepth); + debug = op.getBoolean(SERIALIZER_debug, ctx.debug); + detectRecursions = op.getBoolean(SERIALIZER_detectRecursions, ctx.detectRecursions); + ignoreRecursions = op.getBoolean(SERIALIZER_ignoreRecursions, ctx.ignoreRecursions); + useIndentation = op.getBoolean(SERIALIZER_useIndentation, ctx.useIndentation); + addClassAttrs = op.getBoolean(SERIALIZER_addClassAttrs, ctx.addClassAttrs); + trimNulls = op.getBoolean(SERIALIZER_trimNullProperties, ctx.trimNulls); + trimEmptyLists = op.getBoolean(SERIALIZER_trimEmptyLists, ctx.trimEmptyLists); + trimEmptyMaps = op.getBoolean(SERIALIZER_trimEmptyMaps, ctx.trimEmptyMaps); + trimStrings = op.getBoolean(SERIALIZER_trimStrings, ctx.trimStrings); + quoteChar = op.getString(SERIALIZER_quoteChar, ""+ctx.quoteChar).charAt(0); + relativeUriBase = op.getString(SERIALIZER_relativeUriBase, ctx.relativeUriBase); + absolutePathUriBase = op.getString(SERIALIZER_absolutePathUriBase, ctx.absolutePathUriBase); + sortCollections = op.getBoolean(SERIALIZER_sortCollections, ctx.sortMaps); + sortMaps = op.getBoolean(SERIALIZER_sortMaps, ctx.sortMaps); + } + + this.indent = initialDepth; + if (detectRecursions || debug) { + set = new IdentityHashMap<Object,Object>(); + } else { + set = Collections.emptyMap(); + } + } + + /** + * Wraps the specified output object inside an output stream. + * Subclasses can override this method to implement their own specialized output streams. + * <p> + * This method can be used if the output object is any of the following class types: + * <ul> + * <li>{@link OutputStream} + * <li>{@link File} + * </ul> + * + * @return The output object wrapped in an output stream. + * @throws Exception If object could not be converted to an output stream. + */ + public OutputStream getOutputStream() throws Exception { + if (output == null) + throw new SerializeException("Output cannot be null."); + if (output instanceof OutputStream) + return (OutputStream)output; + if (output instanceof File) { + if (outputStream == null) + outputStream = new BufferedOutputStream(new FileOutputStream((File)output)); + return outputStream; + } + throw new SerializeException("Cannot convert object of type {0} to an OutputStream.", output.getClass().getName()); + } + + + /** + * Wraps the specified output object inside a writer. + * Subclasses can override this method to implement their own specialized writers. + * <p> + * This method can be used if the output object is any of the following class types: + * <ul> + * <li>{@link Writer} + * <li>{@link OutputStream} - Output will be written as UTF-8 encoded stream. + * <li>{@link File} - Output will be written as system-default encoded stream. + * </ul> + * + * @return The output object wrapped in a Writer. + * @throws Exception If object could not be converted to a writer. + */ + public Writer getWriter() throws Exception { + if (output == null) + throw new SerializeException("Output cannot be null."); + if (output instanceof Writer) + return (Writer)output; + if (output instanceof OutputStream) { + if (flushOnlyWriter == null) + flushOnlyWriter = new OutputStreamWriter((OutputStream)output, IOUtils.UTF8); + return flushOnlyWriter; + } + if (output instanceof File) { + if (writer == null) + writer = new OutputStreamWriter(new BufferedOutputStream(new FileOutputStream((File)output))); + return writer; + } + throw new SerializeException("Cannot convert object of type {0} to a Writer.", output.getClass().getName()); + } + + /** + * Returns the raw output object passed into this session. + * + * @return The raw output object passed into this session. + */ + protected Object getOutput() { + return output; + } + + /** + * Sets the current bean property being serialized for proper error messages. + * @param currentProperty The current property being serialized. + */ + public void setCurrentProperty(BeanPropertyMeta<?> currentProperty) { + this.currentProperty = currentProperty; + } + + /** + * Sets the current class being serialized for proper error messages. + * @param currentClass The current class being serialized. + */ + public void setCurrentClass(ClassMeta<?> currentClass) { + this.currentClass = currentClass; + } + + /** + * Returns the bean context in use for this session. + * + * @return The bean context in use for this session. + */ + public final BeanContext getBeanContext() { + return beanContext; + } + + /** + * Returns the Java method that invoked this serializer. + * <p> + * 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. + */ + public final Method getJavaMethod() { + return javaMethod; + } + + /** + * Returns the override properties passed in to the constructor. + * + * @return The override properties pass in to the constructor. + */ + public final ObjectMap getProperties() { + return overrideProperties; + } + + /** + * Returns the {@link SerializerContext#SERIALIZER_maxDepth} setting value for this session. + * + * @return The {@link SerializerContext#SERIALIZER_maxDepth} setting value for this session. + */ + public final int getMaxDepth() { + return maxDepth; + } + + /** + * Returns the {@link SerializerContext#SERIALIZER_initialDepth} setting value for this session. + * + * @return The {@link SerializerContext#SERIALIZER_initialDepth} setting value for this session. + */ + public final int getInitialDepth() { + return initialDepth; + } + + /** + * Returns the {@link SerializerContext#SERIALIZER_debug} setting value for this session. + * + * @return The {@link SerializerContext#SERIALIZER_debug} setting value for this session. + */ + public final boolean isDebug() { + return debug; + } + + /** + * Returns the {@link SerializerContext#SERIALIZER_detectRecursions} setting value for this session. + * + * @return The {@link SerializerContext#SERIALIZER_detectRecursions} setting value for this session. + */ + public final boolean isDetectRecursions() { + return detectRecursions; + } + + /** + * Returns the {@link SerializerContext#SERIALIZER_ignoreRecursions} setting value for this session. + * + * @return The {@link SerializerContext#SERIALIZER_ignoreRecursions} setting value for this session. + */ + public final boolean isIgnoreRecursions() { + return ignoreRecursions; + } + + /** + * Returns the {@link SerializerContext#SERIALIZER_useIndentation} setting value for this session. + * + * @return The {@link SerializerContext#SERIALIZER_useIndentation} setting value for this session. + */ + public final boolean isUseIndentation() { + return useIndentation; + } + + /** + * Returns the {@link SerializerContext#SERIALIZER_addClassAttrs} setting value for this session. + * + * @return The {@link SerializerContext#SERIALIZER_addClassAttrs} setting value for this session. + */ + public final boolean isAddClassAttrs() { + return addClassAttrs; + } + + /** + * Returns the {@link SerializerContext#SERIALIZER_quoteChar} setting value for this session. + * + * @return The {@link SerializerContext#SERIALIZER_quoteChar} setting value for this session. + */ + public final char getQuoteChar() { + return quoteChar; + } + + /** + * Returns the {@link SerializerContext#SERIALIZER_trimNullProperties} setting value for this session. + * + * @return The {@link SerializerContext#SERIALIZER_trimNullProperties} setting value for this session. + */ + public final boolean isTrimNulls() { + return trimNulls; + } + + /** + * Returns the {@link SerializerContext#SERIALIZER_trimEmptyLists} setting value for this session. + * + * @return The {@link SerializerContext#SERIALIZER_trimEmptyLists} setting value for this session. + */ + public final boolean isTrimEmptyLists() { + return trimEmptyLists; + } + + /** + * Returns the {@link SerializerContext#SERIALIZER_trimEmptyMaps} setting value for this session. + * + * @return The {@link SerializerContext#SERIALIZER_trimEmptyMaps} setting value for this session. + */ + public final boolean isTrimEmptyMaps() { + return trimEmptyMaps; + } + + /** + * Returns the {@link SerializerContext#SERIALIZER_trimStrings} setting value for this session. + * + * @return The {@link SerializerContext#SERIALIZER_trimStrings} setting value for this session. + */ + public final boolean isTrimStrings() { + return trimStrings; + } + + /** + * Returns the {@link SerializerContext#SERIALIZER_sortCollections} setting value for this session. + * + * @return The {@link SerializerContext#SERIALIZER_sortCollections} setting value for this session. + */ + public final boolean isSortCollections() { + return sortCollections; + } + + /** + * Returns the {@link SerializerContext#SERIALIZER_sortMaps} setting value for this session. + * + * @return The {@link SerializerContext#SERIALIZER_sortMaps} setting value for this session. + */ + public final boolean isSortMaps() { + return sortMaps; + } + + /** + * Returns the {@link SerializerContext#SERIALIZER_relativeUriBase} setting value for this session. + * + * @return The {@link SerializerContext#SERIALIZER_relativeUriBase} setting value for this session. + */ + public final String getRelativeUriBase() { + return relativeUriBase; + } + + /** + * Returns the {@link SerializerContext#SERIALIZER_absolutePathUriBase} setting value for this session. + * + * @return The {@link SerializerContext#SERIALIZER_absolutePathUriBase} setting value for this session. + */ + public final String getAbsolutePathUriBase() { + return absolutePathUriBase; + } + + /** + * Push the specified object onto the stack. + * + * @param attrName The attribute name. + * @param o The current object being serialized. + * @param eType The expected class type. + * @return The {@link ClassMeta} of the object so that <code>instanceof</code> operations + * only need to be performed once (since they can be expensive).<br> + * @throws SerializeException If recursion occurred. + */ + public ClassMeta<?> push(String attrName, Object o, ClassMeta<?> eType) throws SerializeException { + indent++; + isBottom = true; + if (o == null) + return null; + Class<?> c = o.getClass(); + ClassMeta<?> cm = (eType != null && c == eType.getInnerClass()) ? eType : beanContext.getClassMeta(c); + if (cm.isCharSequence() || cm.isNumber() || cm.isBoolean()) + return cm; + if (detectRecursions || debug) { + if (stack.size() > maxDepth) + return null; + if (willRecurse(attrName, o, cm)) + return null; + isBottom = false; + stack.add(new StackElement(stack.size(), attrName, o, cm)); + if (debug) + logger.info(getStack(false)); + set.put(o, o); + } + return cm; + } + + /** + * Returns <jk>true</jk> if {@link SerializerContext#SERIALIZER_detectRecursions} is enabled, and the specified + * object is already higher up in the serialization chain. + * + * @param attrName The bean property attribute name, or some other identifier. + * @param o The object to check for recursion. + * @param cm The metadata on the object class. + * @return <jk>true</jk> if recursion detected. + * @throws SerializeException If recursion occurred. + */ + public boolean willRecurse(String attrName, Object o, ClassMeta<?> cm) throws SerializeException { + if (! (detectRecursions || debug)) + return false; + if (! set.containsKey(o)) + return false; + if (ignoreRecursions && ! debug) + return true; + + stack.add(new StackElement(stack.size(), attrName, o, cm)); + throw new SerializeException("Recursion occurred, stack={0}", getStack(true)); + } + + /** + * Pop an object off the stack. + */ + public void pop() { + indent--; + if ((detectRecursions || debug) && ! isBottom) { + Object o = stack.removeLast().o; + Object o2 = set.remove(o); + if (o2 == null) + addWarning("Couldn't remove object of type ''{0}'' on attribute ''{1}'' from object stack.", o.getClass().getName(), stack); + } + isBottom = false; + } + + /** + * The current indentation depth. + * + * @return The current indentation depth. + */ + public int getIndent() { + return indent; + } + + /** + * Logs a warning message. + * + * @param msg The warning message. + * @param args Optional printf arguments to replace in the error message. + */ + public void addWarning(String msg, Object... args) { + logger.warning(msg, args); + msg = args.length == 0 ? msg : MessageFormat.format(msg, args); + warnings.add((warnings.size() + 1) + ": " + msg); + } + + /** + * 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. + */ + public void addBeanGetterWarning(BeanPropertyMeta<?> p, Throwable t) { + String prefix = (debug ? getStack(false) + ": " : ""); + addWarning("{0}Could not call getValue() on property ''{1}'' of class ''{2}'', exception = {3}", prefix, p.getName(), p.getBeanMeta().getClassMeta(), t.getLocalizedMessage()); + } + + /** + * Trims the specified string if {@link SerializerSession#isTrimStrings()} returns <jk>true</jk>. + * + * @param o The input string to trim. + * @return The trimmed string, or <jk>null</jk> if the input was <jk>null</jk>. + */ + public final String trim(Object o) { + if (o == null) + return null; + String s = o.toString(); + if (trimStrings) + s = s.trim(); + return s; + } + + /** + * Generalize the specified object if a transform is associated with it. + * + * @param o The object to generalize. + * @param type The type of object. + * @return The generalized object, or <jk>null</jk> if the object is <jk>null</jk>. + * @throws SerializeException If a problem occurred trying to convert the output. + */ + @SuppressWarnings({ "rawtypes", "unchecked" }) + public final Object generalize(Object o, ClassMeta<?> type) throws SerializeException { + if (o == null) + return null; + PojoTransform f = (type == null || type.isObject() ? getBeanContext().getClassMeta(o.getClass()).getPojoTransform() : type.getPojoTransform()); + if (f == null) + return o; + return f.transform(o); + } + + /** + * Returns <jk>true</jk> 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 <jk>null</jk> if this isn't a bean attribute. + * @param value The object being serialized. + * @return <jk>true</jk> 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 (trimNulls && value == null) + return true; + + if (value == null) + return false; + + if (cm == null) + cm = getBeanContext().object(); + + if (trimEmptyLists) { + 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 (trimEmptyMaps) { + if (cm.isMap() || (cm.isObject() && isParentClass(Map.class, value.getClass()))) { + if (((Map<?,?>)value).isEmpty()) + return true; + } + } + + if (trimNulls && willRecurse(attrName, value, cm)) + return true; + + return false; + } + + /** + * Sorts the specified map if {@link SerializerSession#isSortMaps()} returns <jk>true</jk>. + * + * @param m The map being sorted. + * @return A new sorted {@link TreeMap}. + */ + public final <K,V> Map<K,V> sort(Map<K,V> m) { + if (sortMaps && m != null && (! m.isEmpty()) && m.keySet().iterator().next() instanceof Comparable<?>) + return new TreeMap<K,V>(m); + return m; + } + + /** + * Sorts the specified collection if {@link SerializerSession#isSortCollections()} returns <jk>true</jk>. + * + * @param c The collection being sorted. + * @return A new sorted {@link TreeSet}. + */ + public final <E> Collection<E> sort(Collection<E> c) { + if (sortCollections && c != null && (! c.isEmpty()) && c.iterator().next() instanceof Comparable<?>) + return new TreeSet<E>(c); + return c; + } + + /** + * Converts a String to an absolute URI based on the {@link SerializerContext#SERIALIZER_absolutePathUriBase} and + * {@link SerializerContext#SERIALIZER_relativeUriBase} settings on this context. + * + * @param uri The input URI. + * @return The resolved URI. + */ + public String resolveUri(String uri) { + if (uri.indexOf("://") != -1 || (absolutePathUriBase == null && relativeUriBase == null)) + return uri; + StringBuilder sb = new StringBuilder(uri.length() + absolutePathUriBase.length() + 1 + relativeUriBase.length()); + if (StringUtils.startsWith(uri, '/')) { + if (absolutePathUriBase != null) + sb.append(absolutePathUriBase); + } else { + if (relativeUriBase != null) { + sb.append(relativeUriBase); + if (! uri.equals("/")) + sb.append("/"); + } + } + sb.append(uri); + return sb.toString(); + } + + /** + * Converts the specified object to a <code>String</code>. + * + * @param o The object to convert to a <code>String</code>. + * @return The + */ + public String toString(Object o) { + if (o == null) + return null; + if (o.getClass() == Class.class) + return ClassUtils.getReadableClassName((Class<?>)o); + String s = o.toString(); + if (trimStrings) + s = s.trim(); + return s; + } + + /** + * Perform cleanup on this context object if necessary. + * + * @throws SerializeException If we're in debug mode and one or more warnings occurred. + */ + public void close() throws SerializeException { + if (closed) + throw new SerializeException("Attempt to close SerializerSession more than once."); + + try { + if (outputStream != null) + outputStream.close(); + if (flushOnlyWriter != null) + flushOnlyWriter.flush(); + if (writer != null) + writer.close(); + } catch (IOException e) { + throw new SerializeException(e); + } + + if (debug && warnings.size() > 0) + throw new SerializeException("Warnings occurred during serialization: \n" + StringUtils.join(warnings, "\n")); + + closed = true; + } + + @Override /* Object */ + protected void finalize() throws Throwable { + if (! closed) + throw new RuntimeException("SerializerSession was not closed."); + } + + private static class StackElement { + private int depth; + private String name; + private Object o; + private ClassMeta<?> aType; + + private StackElement(int depth, String name, Object o, ClassMeta<?> aType) { + this.depth = depth; + this.name = name; + this.o = o; + this.aType = aType; + } + + private String toString(boolean simple) { + StringBuilder sb = new StringBuilder().append('[').append(depth).append(']'); + sb.append(StringUtils.isEmpty(name) ? "<noname>" : name).append(':'); + sb.append(aType.toString(simple)); + if (aType != aType.getTransformedClassMeta()) + sb.append('/').append(aType.getTransformedClassMeta().toString(simple)); + return sb.toString(); + } + } + + private String getStack(boolean full) { + StringBuilder sb = new StringBuilder(); + for (StackElement e : stack) { + if (full) { + sb.append("\n\t"); + for (int i = 1; i < e.depth; i++) + sb.append(" "); + if (e.depth > 0) + sb.append("->"); + sb.append(e.toString(false)); + } else { + sb.append(" > ").append(e.toString(true)); + } + } + return sb.toString(); + } + + /** + * Returns information used to determine at what location in the parse a failure occurred. + * + * @return A map, typically containing something like <code>{line:123,column:456,currentProperty:"foobar"}</code> + */ + public Map<String,Object> getLastLocation() { + Map<String,Object> m = new LinkedHashMap<String,Object>(); + if (currentClass != null) + m.put("currentClass", currentClass); + if (currentProperty != null) + m.put("currentProperty", currentProperty); + if (stack != null && ! stack.isEmpty()) + m.put("stack", stack); + return m; + } +}
