/**
 * Copyright 2005-2013 Restlet S.A.S.
 * 
 * The contents of this file are subject to the terms of one of the following
 * open source licenses: Apache 2.0 or LGPL 3.0 or LGPL 2.1 or CDDL 1.0 or EPL
 * 1.0 (the "Licenses"). You can select the license that you prefer but you may
 * not use this file except in compliance with one of these Licenses.
 * 
 * You can obtain a copy of the Apache 2.0 license at
 * http://www.opensource.org/licenses/apache-2.0
 * 
 * You can obtain a copy of the LGPL 3.0 license at
 * http://www.opensource.org/licenses/lgpl-3.0
 * 
 * You can obtain a copy of the LGPL 2.1 license at
 * http://www.opensource.org/licenses/lgpl-2.1
 * 
 * You can obtain a copy of the CDDL 1.0 license at
 * http://www.opensource.org/licenses/cddl1
 * 
 * You can obtain a copy of the EPL 1.0 license at
 * http://www.opensource.org/licenses/eclipse-1.0
 * 
 * See the Licenses for the specific language governing permissions and
 * limitations under the Licenses.
 * 
 * Alternatively, you can obtain a royalty free commercial license with less
 * limitations, transferable or non-transferable, directly at
 * http://www.restlet.com/products/restlet-framework
 * 
 * Restlet is a registered trademark of Restlet S.A.S.
 */

package org.restlet.ext.jaxb;

import java.io.IOException;
import java.io.Writer;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Level;

import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.ValidationEventHandler;
import javax.xml.bind.util.JAXBSource;
import javax.xml.parsers.DocumentBuilderFactory;

import org.restlet.Context;
import org.restlet.data.MediaType;
import org.restlet.ext.jaxb.internal.Marshaller;
import org.restlet.ext.jaxb.internal.Unmarshaller;
import org.restlet.representation.Representation;
import org.restlet.representation.WriterRepresentation;

/**
 * An XML representation based on JAXB that provides easy translation between
 * XML and JAXB element class trees.
 * 
 * @author Overstock.com
 * @author Jerome Louvel
 * @param <T>
 *            The type to wrap.
 */
public class JaxbRepresentation<T> extends WriterRepresentation {

    /** Improves performance by caching contexts which are expensive to create. */
    private final static Map<String, JAXBContext> contexts = new ConcurrentHashMap<String, JAXBContext>();

    /**
     * Returns the JAXB context, if possible from the cached contexts.
     * 
     * @param contextPath
     *            The JAXB context path.
     * 
     * @return The JAXB context.
     * @throws JAXBException
     */
    public static synchronized JAXBContext getContext(String contextPath)
            throws JAXBException {
        return getContext(contextPath, null);
    }

    /**
     * Returns the JAXB context, if possible from the cached contexts.
     * 
     * @param contextPath
     *            The JAXB context path.
     * 
     * @param classLoader
     *            The JAXB classloader to use for annotated JAXB classes.
     * 
     * @return The JAXB context.
     * @throws JAXBException
     */
    public static synchronized JAXBContext getContext(String contextPath,
            ClassLoader classLoader) throws JAXBException {
        // Contexts are thread-safe so reuse those.
        JAXBContext result = contexts.get(contextPath);

        if (result == null) {
            result = (classLoader == null) ? JAXBContext
                    .newInstance(contextPath) : JAXBContext.newInstance(
                    contextPath, classLoader);
            contexts.put(contextPath, result);
        }

        return result;
    }

    /**
     * The classloader to use for JAXB annotated classes.
     */
    private volatile ClassLoader classLoader;

    /**
     * The list of Java package names that contain schema derived class and/or
     * Java to schema (JAXB-annotated) mapped classes.
     */
    private volatile String contextPath;

    /**
     * Specifies that the parser will expand entity reference nodes. By default
     * the value of this is set to true.
     */
    private volatile boolean expandingEntityRefs;

    /**
     * Indicates if the resulting XML data should be formatted with line breaks
     * and indentation. Defaults to false.
     */
    private volatile boolean formattedOutput;

    /**
     * Indicates whether or not document level events will be generated by the
     * Marshaller.
     */
    private volatile boolean fragment;

    /** The "xsi:noNamespaceSchemaLocation" attribute in the generated XML data. */
    private volatile String noNamespaceSchemaLocation;

    /** The wrapped Java object. */
    private volatile T object;

    /** The "xsi:schemaLocation" attribute in the generated XML data */
    private volatile String schemaLocation;

    /** Limits potential XML overflow attacks. */
    private boolean secureProcessing;

    /**
     * Indicates the desire for validating this type of XML representations
     * against a DTD. Note that for XML schema or Relax NG validation, use the
     * "schema" property instead.
     * 
     * @see DocumentBuilderFactory#setValidating(boolean)
     */
    private volatile boolean validatingDtd;

    /** The JAXB validation event handler. */
    private volatile ValidationEventHandler validationEventHandler;

    /**
     * Indicates the desire for processing <em>XInclude</em> if found in this
     * type of XML representations. By default the value of this is set to
     * false.
     * 
     * @see DocumentBuilderFactory#setXIncludeAware(boolean)
     */
    private volatile boolean xIncludeAware;

    /** The source XML representation. */
    private volatile Representation xmlRepresentation;

    /**
     * Creates a JAXB representation from an existing JAXB content tree.
     * 
     * @param mediaType
     *            The representation's media type.
     * @param object
     *            The Java object.
     */
    public JaxbRepresentation(MediaType mediaType, T object) {
        this(mediaType, object, (object != null) ? object.getClass()
                .getClassLoader() : null);
    }

    /**
     * Creates a JAXB representation from an existing JAXB content tree.
     * 
     * @param mediaType
     *            The representation's media type.
     * @param object
     *            The Java object.
     * @param classloader
     *            The classloader to use for JAXB annotated classes.
     */
    public JaxbRepresentation(MediaType mediaType, T object,
            ClassLoader classloader) {
        super(mediaType);
        this.classLoader = classloader;
        this.contextPath = (object != null) ? object.getClass().getPackage()
                .getName() : null;
        this.object = object;
        this.validationEventHandler = null;
        this.xmlRepresentation = null;
        this.expandingEntityRefs = false;
        this.formattedOutput = false;
        this.fragment = false;
        this.noNamespaceSchemaLocation = null;
        this.schemaLocation = null;
        this.secureProcessing = true;
        this.validatingDtd = false;
        this.xIncludeAware = false;
    }

    /**
     * Creates a new JAXB representation, converting the input XML into a Java
     * content tree. The XML is validated.
     * 
     * @param xmlRepresentation
     *            The XML wrapped in a representation.
     * @param type
     *            The type to convert to.
     * 
     * @throws JAXBException
     *             If the incoming XML does not validate against the schema.
     * @throws IOException
     *             If unmarshalling XML fails.
     */
    public JaxbRepresentation(Representation xmlRepresentation, Class<T> type) {
        this(xmlRepresentation, type.getPackage().getName(), null, type
                .getClassLoader());
    }

    /**
     * Creates a new JAXB representation, converting the input XML into a Java
     * content tree. The XML is validated.
     * 
     * @param xmlRepresentation
     *            The XML wrapped in a representation.
     * @param type
     *            The type to convert to.
     * @param validationHandler
     *            A handler for dealing with validation failures.
     * 
     * @throws JAXBException
     *             If the incoming XML does not validate against the schema.
     * @throws IOException
     *             If unmarshalling XML fails.
     */
    public JaxbRepresentation(Representation xmlRepresentation, Class<T> type,
            ValidationEventHandler validationHandler) {
        this(xmlRepresentation, type.getPackage().getName(), validationHandler,
                type.getClassLoader());
    }

    /**
     * Creates a new JAXB representation, converting the input XML into a Java
     * content tree. The XML is validated.
     * 
     * @param xmlRepresentation
     *            The XML wrapped in a representation.
     * @param contextPath
     *            The list of Java package names for JAXB.
     * 
     * @throws JAXBException
     *             If the incoming XML does not validate against the schema.
     * @throws IOException
     *             If unmarshalling XML fails.
     */
    public JaxbRepresentation(Representation xmlRepresentation,
            String contextPath) {
        this(xmlRepresentation, contextPath, null, null);
    }

    /**
     * Creates a new JAXB representation, converting the input XML into a Java
     * content tree. The XML is validated.
     * 
     * @param xmlRepresentation
     *            The XML wrapped in a representation.
     * @param contextPath
     *            The list of Java package names for JAXB.
     * @param validationHandler
     *            A handler for dealing with validation failures.
     * 
     * @throws JAXBException
     *             If the incoming XML does not validate against the schema.
     * @throws IOException
     *             If unmarshalling XML fails.
     */
    public JaxbRepresentation(Representation xmlRepresentation,
            String contextPath, ValidationEventHandler validationHandler) {
        this(xmlRepresentation, contextPath, validationHandler, null);
    }

    /**
     * Creates a new JAXB representation, converting the input XML into a Java
     * content tree. The XML is validated.
     * 
     * @param xmlRepresentation
     *            The XML wrapped in a representation.
     * @param contextPath
     *            The list of Java package names for JAXB.
     * @param validationHandler
     *            A handler for dealing with validation failures.
     * @param classLoader
     *            The classloader to use for JAXB annotated classes.
     * @throws JAXBException
     *             If the incoming XML does not validate against the schema.
     * @throws IOException
     *             If unmarshalling XML fails.
     */
    public JaxbRepresentation(Representation xmlRepresentation,
            String contextPath, ValidationEventHandler validationHandler,
            ClassLoader classLoader) {
        super((xmlRepresentation == null) ? null : xmlRepresentation
                .getMediaType());
        this.classLoader = classLoader;
        this.contextPath = contextPath;
        this.object = null;
        this.validationEventHandler = validationHandler;
        this.xmlRepresentation = xmlRepresentation;
    }

    /**
     * Creates a JAXB representation from an existing JAXB content tree with
     * {@link MediaType#APPLICATION_XML}.
     * 
     * @param object
     *            The Java object.
     */
    public JaxbRepresentation(T object) {
        this(MediaType.APPLICATION_XML, object);
    }

    /**
     * Returns the classloader to use for JAXB annotated classes.
     * 
     * @return The classloader to use for JAXB annotated classes.
     */
    public ClassLoader getClassLoader() {
        return this.classLoader;
    }

    /**
     * Returns the JAXB context.
     * 
     * @return The JAXB context.
     * @throws JAXBException
     */
    public JAXBContext getContext() throws JAXBException {
        return getContext(getContextPath(), getClassLoader());
    }

    /**
     * Returns the list of Java package names that contain schema derived class
     * and/or Java to schema (JAXB-annotated) mapped classes
     * 
     * @return The list of Java package names.
     */
    public String getContextPath() {
        return this.contextPath;
    }

    /**
     * Returns a JAXB SAX source.
     * 
     * @return A JAXB SAX source.
     */
    public JAXBSource getJaxbSource() throws IOException {
        try {
            return new JAXBSource(getContext(), getObject());
        } catch (JAXBException e) {
            throw new IOException(
                    "JAXBException while creating the JAXBSource: "
                            + e.getMessage());
        }
    }

    /**
     * Returns the "xsi:noNamespaceSchemaLocation" attribute in the generated
     * XML data.
     * 
     * @return The "xsi:noNamespaceSchemaLocation" attribute in the generated
     *         XML data.
     */
    public String getNoNamespaceSchemaLocation() {
        return noNamespaceSchemaLocation;
    }

    /**
     * Returns the wrapped Java object.
     * 
     * @return The wrapped Java object.
     * @throws IOException
     */
    @SuppressWarnings("unchecked")
    public T getObject() throws IOException {
        if ((this.object == null) && (this.xmlRepresentation != null)) {
            // Try to unmarshal the wrapped XML representation
            final Unmarshaller<T> u = new Unmarshaller<T>(this.contextPath,
                    this.classLoader);
            if (getValidationEventHandler() != null) {
                try {
                    u.setEventHandler(getValidationEventHandler());
                } catch (JAXBException e) {
                    Context.getCurrentLogger().log(Level.WARNING,
                            "Unable to set the event handler", e);
                    throw new IOException("Unable to set the event handler."
                            + e.getMessage());
                }
            }

            try {
                this.object = (T) u.unmarshal(this,
                        this.xmlRepresentation.getReader());
            } catch (JAXBException e) {
                Context.getCurrentLogger().log(Level.WARNING,
                        "Unable to unmarshal the XML representation", e);
                throw new IOException(
                        "Unable to unmarshal the XML representation."
                                + e.getMessage());
            }
        }
        return this.object;
    }

    /**
     * Returns the "xsi:schemaLocation" attribute in the generated XML data.
     * 
     * @return The "xsi:schemaLocation" attribute in the generated XML data.
     */
    public String getSchemaLocation() {
        return schemaLocation;
    }

    /**
     * Returns the optional validation event handler.
     * 
     * @return The optional validation event handler.
     */
    public ValidationEventHandler getValidationEventHandler() {
        return this.validationEventHandler;
    }

    /**
     * Indicates if the parser will expand entity reference nodes. By default
     * the value of this is set to true.
     * 
     * @return True if the parser will expand entity reference nodes.
     */
    public boolean isExpandingEntityRefs() {
        return expandingEntityRefs;
    }

    /**
     * Indicates if the resulting XML data should be formatted with line breaks
     * and indentation. Defaults to false.
     * 
     * @return the formattedOutput
     */
    public boolean isFormattedOutput() {
        return this.formattedOutput;
    }

    /**
     * Indicates whether or not document level events will be generated by the
     * Marshaller.
     * 
     * @return True if the document level events will be generated by the
     *         Marshaller.
     */
    public boolean isFragment() {
        return fragment;
    }

    /**
     * Indicates if it limits potential XML overflow attacks.
     * 
     * @return True if it limits potential XML overflow attacks.
     */
    public boolean isSecureProcessing() {
        return secureProcessing;
    }

    /**
     * Indicates the desire for validating this type of XML representations
     * against an XML schema if one is referenced within the contents.
     * 
     * @return True if the schema-based validation is enabled.
     */
    public boolean isValidatingDtd() {
        return validatingDtd;
    }

    /**
     * Indicates the desire for processing <em>XInclude</em> if found in this
     * type of XML representations. By default the value of this is set to
     * false.
     * 
     * @return The current value of the xIncludeAware flag.
     */
    public boolean isXIncludeAware() {
        return xIncludeAware;
    }

    /**
     * Sets the classloader to use for JAXB annotated classes.
     * 
     * @param classLoader
     *            The classloader to use for JAXB annotated classes.
     */
    public void setClassLoader(ClassLoader classLoader) {
        this.classLoader = classLoader;
    }

    /**
     * Sets the list of Java package names that contain schema derived class
     * and/or Java to schema (JAXB-annotated) mapped classes.
     * 
     * @param contextPath
     *            The JAXB context path.
     */
    public void setContextPath(String contextPath) {
        this.contextPath = contextPath;
    }

    /**
     * Indicates if the parser will expand entity reference nodes. By default
     * the value of this is set to true.
     * 
     * @param expandEntityRefs
     *            True if the parser will expand entity reference nodes.
     */
    public void setExpandingEntityRefs(boolean expandEntityRefs) {
        this.expandingEntityRefs = expandEntityRefs;
    }

    /**
     * Indicates if the resulting XML data should be formatted with line breaks
     * and indentation.
     * 
     * @param formattedOutput
     *            True if the resulting XML data should be formatted.
     */
    public void setFormattedOutput(boolean formattedOutput) {
        this.formattedOutput = formattedOutput;
    }

    /**
     * Indicates whether or not document level events will be generated by the
     * Marshaller.
     * 
     * @param fragment
     *            True if the document level events will be generated by the
     *            Marshaller.
     */
    public void setFragment(boolean fragment) {
        this.fragment = fragment;
    }

    /**
     * Sets the "xsi:noNamespaceSchemaLocation" attribute in the generated XML
     * data.
     * 
     * @param noNamespaceSchemaLocation
     *            The "xsi:noNamespaceSchemaLocation" attribute in the generated
     *            XML data.
     */
    public void setNoNamespaceSchemaLocation(String noNamespaceSchemaLocation) {
        this.noNamespaceSchemaLocation = noNamespaceSchemaLocation;
    }

    /**
     * Sets the wrapped Java object.
     * 
     * @param object
     *            The Java object to set.
     */
    public void setObject(T object) {
        this.object = object;
    }

    /**
     * Sets the "xsi:schemaLocation" attribute in the generated XML data.
     * 
     * @param schemaLocation
     *            The "xsi:noNamespaceSchemaLocation" attribute in the generated
     *            XML data.
     */
    public void setSchemaLocation(String schemaLocation) {
        this.schemaLocation = schemaLocation;
    }

    /**
     * Indicates if it limits potential XML overflow attacks.
     * 
     * @param secureProcessing
     *            True if it limits potential XML overflow attacks.
     */
    public void setSecureProcessing(boolean secureProcessing) {
        this.secureProcessing = secureProcessing;
    }

    /**
     * Indicates the desire for validating this type of XML representations
     * against an XML schema if one is referenced within the contents.
     * 
     * @param validating
     *            The new validation flag to set.
     */
    public void setValidatingDtd(boolean validating) {
        this.validatingDtd = validating;
    }

    /**
     * Sets the validation event handler.
     * 
     * @param validationEventHandler
     *            The optional validation event handler.
     */
    public void setValidationEventHandler(
            ValidationEventHandler validationEventHandler) {
        this.validationEventHandler = validationEventHandler;
    }

    /**
     * Indicates the desire for processing <em>XInclude</em> if found in this
     * type of XML representations. By default the value of this is set to
     * false.
     * 
     * @param includeAware
     *            The new value of the xIncludeAware flag.
     */
    public void setXIncludeAware(boolean includeAware) {
        xIncludeAware = includeAware;
    }

    /**
     * Writes the representation to a stream of characters.
     * 
     * @param writer
     *            The writer to use when writing.
     * 
     * @throws IOException
     *             If any error occurs attempting to write the stream.
     */
    @Override
    public void write(Writer writer) throws IOException {
        try {
            new Marshaller<T>(this, this.contextPath, getClassLoader())
                    .marshal(getObject(), writer);
        } catch (JAXBException e) {
            Context.getCurrentLogger().log(Level.WARNING,
                    "JAXB marshalling error caught.", e);

            // Maybe the tree represents a failure, try that.
            try {
                new Marshaller<T>(this, "failure", getClassLoader()).marshal(
                        getObject(), writer);
            } catch (JAXBException e2) {
                // We don't know what package this tree is from.
                throw new IOException(e.getMessage());
            }
        }
    }

}
