blob: 7d378bbb15cb2bb1408056beb9b38c8cf048265b [file] [log] [blame]
/*
* Copyright (c) 2011, 2021 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 v. 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0,
* or the Eclipse Distribution License v. 1.0 which is available at
* http://www.eclipse.org/org/documents/edl-v10.php.
*
* SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause
*/
// 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 jakarta.ws.rs.WebApplicationException;
import jakarta.ws.rs.core.HttpHeaders;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.StreamingOutput;
import jakarta.xml.bind.JAXBContext;
import jakarta.xml.bind.JAXBException;
import jakarta.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, StandardCharsets.UTF_8);
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;
}
}