/*******************************************************************************
 * Copyright (c) 2011, 2016 Oracle and/or its affiliates. All rights reserved.
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License v1.0 and Eclipse Distribution License v. 1.0
 * which accompanies this distribution.
 * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v10.html
 * and the Eclipse Distribution License is available at
 * http://www.eclipse.org/org/documents/edl-v10.php.
 *
 * Contributors:
 * 		dclarke/tware - Initial implementation
 *      09-01-2014-2.6.0 Dmitry Kornilov
 *        - Fields filtering (projection), application/schema+json media type handling
 ******************************************************************************/
package org.eclipse.persistence.jpa.rs.util;

import java.beans.PropertyChangeListener;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.nio.charset.StandardCharsets;
import java.util.List;

import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.StreamingOutput;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Marshaller;

import org.eclipse.persistence.dynamic.DynamicEntity;
import org.eclipse.persistence.internal.dynamic.DynamicEntityImpl;
import org.eclipse.persistence.jaxb.MarshallerProperties;
import org.eclipse.persistence.jpa.rs.PersistenceContext;
import org.eclipse.persistence.jpa.rs.exceptions.JPARSException;
import org.eclipse.persistence.jpa.rs.features.fieldsfiltering.FieldsFilter;
import org.eclipse.persistence.jpa.rs.resources.common.AbstractResource;
import org.eclipse.persistence.jpa.rs.util.list.ReportQueryResultList;
import org.eclipse.persistence.jpa.rs.util.xmladapters.LinkAdapter;

/**
 * Simple {@link StreamingOutput} implementation that uses the provided
 * {@link JAXBContext} to marshal the result when requested to either XML or
 * JSON based on the accept media provided.
 *
 * @author dclarke
 * @since EclipseLink 2.4.0
 */
public class StreamingOutputMarshaller implements StreamingOutput {
    private final PersistenceContext context;
    private final Object result;
    private final MediaType mediaType;
    private FieldsFilter filter;

    public StreamingOutputMarshaller(PersistenceContext context, Object result, MediaType acceptedType) {
        this.context = context;
        this.result = result;
        this.mediaType = acceptedType;
    }

    /**
     * This constructor is used for fields filtering. Only attributes included in fields parameter are marshalled.
     *
     * @param context persistence context.
     * @param result entity to process.
     * @param acceptedTypes accepted media types.
     * @param filter containing a list of fields to filter out from the response.
     */
    public StreamingOutputMarshaller(PersistenceContext context, Object result, List<MediaType> acceptedTypes, FieldsFilter filter) {
        this(context, result, acceptedTypes);
        this.filter = filter;
    }

    /**
     * Creates a new StreamingOutputMarshaller.
     *
     * @param context persistence context.
     * @param result entity to process.
     * @param acceptedTypes accepted media types.
     */
    public StreamingOutputMarshaller(PersistenceContext context, Object result, List<MediaType> acceptedTypes) {
        this(context, result, mediaType(acceptedTypes));
    }

    @Override
    public void write(OutputStream output) throws IOException, WebApplicationException {
        if (result instanceof byte[] && this.mediaType.equals(MediaType.APPLICATION_OCTET_STREAM_TYPE)) {
            output.write((byte[]) result);
            output.flush();
            output.close();
        } else if (result instanceof String) {
            OutputStreamWriter writer = new OutputStreamWriter(output);
            writer.write((String) result);
            writer.flush();
            writer.close();
        } else {
            if ((this.context != null && this.context.getJAXBContext() != null && this.result != null) &&
                    (this.mediaType.equals(MediaType.APPLICATION_JSON_TYPE) || this.mediaType.equals(MediaType.APPLICATION_XML_TYPE))) {
                try {
                    if (result instanceof ReportQueryResultList) {
                        if (mediaType == MediaType.APPLICATION_JSON_TYPE) {
                            // avoid outer QueryResultList class (outer grouping name) in JSON responses
                            context.marshallEntity(((ReportQueryResultList) result).getItems(), mediaType, output);
                        } else {
                            context.marshallEntity(result, mediaType, output);
                        }
                    } else {
                        if (filter != null) {
                            context.marshallEntity(result, filter, mediaType, output);
                        } else {
                            context.marshallEntity(result, mediaType, output);
                        }
                    }
                    return;
                } catch (Exception ex) {
                    JPARSLogger.exception(context.getSessionLog(), "jpars_caught_exception", new Object[] {}, ex);
                    throw JPARSException.exceptionOccurred(ex);
                }
            }

            if (this.mediaType.equals(MediaType.APPLICATION_OCTET_STREAM_TYPE)) {
                // could not marshall, try serializing
                ByteArrayOutputStream baos = new ByteArrayOutputStream();
                ObjectOutputStream oos = new ObjectOutputStream(baos);
                oos.writeObject(result);
                oos.flush();
                oos.close();
                output.write(baos.toByteArray());
            } else {
                if (context != null) {
                    JPARSLogger.error(context.getSessionLog(), "jpars_could_not_marshal_requested_result_to_requested_type", new Object[] { result });
                } else {
                    JPARSLogger.error("jpars_could_not_marshal_requested_result_to_requested_type", new Object[] { result });
                }
                throw new WebApplicationException();
            }
        }
    }

    /**
     * Identify the preferred {@link MediaType} from the list provided. This
     * will check for JSON string or {@link MediaType} first then XML.
     *
     * @param types
     *            List of {@link MediaType} values;
     * @return selected {@link MediaType}
     */
    public static MediaType mediaType(List<MediaType> types) {
        MediaType aMediaType;
        if (types != null) {
            for (MediaType type : types) {
                aMediaType = type;
                if (aMediaType.isCompatible(MediaType.APPLICATION_JSON_TYPE)) {
                    return MediaType.APPLICATION_JSON_TYPE;
                }
                if (aMediaType.isCompatible(MediaType.APPLICATION_XML_TYPE)) {
                    return MediaType.APPLICATION_XML_TYPE;
                }
                if (aMediaType.isCompatible(MediaType.APPLICATION_OCTET_STREAM_TYPE)) {
                    return MediaType.APPLICATION_OCTET_STREAM_TYPE;
                }
                if (aMediaType.isCompatible(AbstractResource.APPLICATION_SCHEMA_JSON_TYPE)) {
                    return AbstractResource.APPLICATION_SCHEMA_JSON_TYPE;
                }
            }
        }
        // An unsupported media type never comes to resource, no need to throw exception here.
        return MediaType.APPLICATION_JSON_TYPE;
    }

    public static Marshaller createMarshaller(PersistenceContext context, MediaType mediaType) throws JAXBException {
        Marshaller marshaller = context.getJAXBContext().createMarshaller();
        marshaller.setProperty(MarshallerProperties.MEDIA_TYPE, mediaType.toString());
        marshaller.setProperty(MarshallerProperties.JSON_INCLUDE_ROOT, false);
        marshaller.setAdapter(new LinkAdapter(context.getBaseURI().toString(), context));
        marshaller.setListener(new Marshaller.Listener() {
            @Override
            public void beforeMarshal(Object source) {
                if (source instanceof DynamicEntity) {
                    DynamicEntityImpl sourceImpl = (DynamicEntityImpl) source;
                    PropertyChangeListener listener = sourceImpl._persistence_getPropertyChangeListener();
                    sourceImpl._persistence_setPropertyChangeListener(null);
                    ((DynamicEntity) source).set("self", source);
                    sourceImpl._persistence_setPropertyChangeListener(listener);
                }
            }
        });
        return marshaller;
    }

    public static MediaType getResponseMediaType(HttpHeaders headers) {
        MediaType mediaType = MediaType.TEXT_PLAIN_TYPE;
        if (headers != null) {
            List<MediaType> accepts = headers.getAcceptableMediaTypes();
            if (accepts != null && accepts.size() > 0) {
                try {
                    mediaType = StreamingOutputMarshaller.mediaType(accepts);
                } catch (Exception ex) {
                    JPARSLogger.exception("Exception in getResponseMediaType", new Object[]{headers}, ex);
                }
            }
        }
        return mediaType;
    }
}
