Please wait. This can take some minutes ...
Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance.
Project price only 1 $
You can buy this project and download/modify it how often you want.
org.eclipse.persistence.jaxb.rs.MOXyJsonProvider Maven / Gradle / Ivy
/*
* Copyright (c) 2012, 2024 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:
// Blaise Doughan - 2.4 - initial implementation
package org.eclipse.persistence.jaxb.rs;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.Reader;
import java.lang.annotation.Annotation;
import java.lang.reflect.Array;
import java.lang.reflect.GenericArrayType;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.lang.reflect.WildcardType;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Deque;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.NavigableSet;
import java.util.Queue;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;
import jakarta.activation.DataSource;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.WebApplicationException;
import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.MultivaluedMap;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.Response.ResponseBuilder;
import jakarta.ws.rs.core.Response.Status;
import jakarta.ws.rs.core.StreamingOutput;
import jakarta.ws.rs.ext.ContextResolver;
import jakarta.ws.rs.ext.MessageBodyReader;
import jakarta.ws.rs.ext.MessageBodyWriter;
import jakarta.ws.rs.ext.Provider;
import jakarta.ws.rs.ext.Providers;
import jakarta.xml.bind.JAXBContext;
import jakarta.xml.bind.JAXBElement;
import jakarta.xml.bind.JAXBException;
import jakarta.xml.bind.JAXBIntrospector;
import jakarta.xml.bind.Marshaller;
import jakarta.xml.bind.UnmarshalException;
import jakarta.xml.bind.Unmarshaller;
import javax.xml.namespace.QName;
import javax.xml.transform.stream.StreamSource;
import org.eclipse.persistence.exceptions.JSONException;
import org.eclipse.persistence.internal.core.helper.CoreClassConstants;
import org.eclipse.persistence.internal.helper.Helper;
import org.eclipse.persistence.internal.oxm.Constants;
import org.eclipse.persistence.internal.queries.CollectionContainerPolicy;
import org.eclipse.persistence.internal.queries.ContainerPolicy;
import org.eclipse.persistence.jaxb.JAXBContextFactory;
import org.eclipse.persistence.jaxb.MarshallerProperties;
import org.eclipse.persistence.jaxb.UnmarshallerProperties;
import org.eclipse.persistence.logging.AbstractSessionLog;
import org.eclipse.persistence.logging.SessionLog;
import org.eclipse.persistence.oxm.JSONWithPadding;
/**
* This is an implementation of MessageBodyReader /MessageBodyWriter
* that can be used to enable EclipseLink JAXB (MOXy) as the JSON
* provider.
*
* Supported Media Type Patterns
*
* */json (i.e. application/json and text/json)
* */*+json
*
*
* Below are some different usage options.
*
* Option #1 - MOXyJsonProvider Default Behavior
* You can use the Application class to specify that
* MOXyJsonProvider should be used with your JAX-RS application.
*
* package org.example;
* import java.util.*;
* import jakarta.ws.rs.core.Application;
* import org.eclipse.persistence.jaxb.rs.MOXyJsonProvider;
*
* public class ExampleApplication extends Application {
*
* @Override
* public Set<Class<?>> getClasses() {
* HashSet<Class<?>> set = new HashSet<Class<?>>(2);
* set.add(MOXyJsonProvider.class);
* set.add(ExampleService.class);
* return set;
* }
*
* }
*
*
* Option #2 - Customize MOXyJsonProvider
* You can use the Application class to specify a configured instance
* of MOXyJsonProvider should be used with your JAX-RS application.
*
* package org.example;
*
* import java.util.*;
* import jakarta.ws.rs.core.Application;
* import org.eclipse.persistence.jaxb.rs.MOXyJsonProvider;
*
* public class CustomerApplication extends Application {
*
* @Override
* public Set<Class<?>> getClasses() {
* HashSet<Class<?>> set = new HashSet<Class<?>>(1);
* set.add(ExampleService.class);
* return set;
* }
* @Override
* public Set<Object> getSingletons() {
* moxyJsonProvider moxyJsonProvider = new MOXyJsonProvider();
* moxyJsonProvider.setFormattedOutput(true);
* moxyJsonProvider.setIncludeRoot(true);
*
* HashSet<Object> set = new HashSet<Object>(2);
* set.add(moxyJsonProvider);
* return set;
* }
*
* }
*
* Option #3 - Extend MOXyJsonProvider
* You can use MOXyJsonProvider for creating your own
* MessageBodyReader /MessageBodyWriter .
*
* package org.example;
*
* import java.lang.annotation.Annotation;
* import java.lang.reflect.Type;
*
* import jakarta.ws.rs.*;
* import jakarta.ws.rs.core.*;
* import jakarta.ws.rs.ext.Provider;
* import jakarta.xml.bind.*;
*
* import org.eclipse.persistence.jaxb.MarshallerProperties;
* import org.eclipse.persistence.jaxb.rs.MOXyJsonProvider;
*
* @Provider
* @Produces(MediaType.APPLICATION_JSON)
* @Consumes(MediaType.APPLICATION_JSON)
* public class CustomerJSONProvider extends MOXyJsonProvider {
* @Override
* public boolean isReadable(Class<?> type, Type genericType,
* Annotation[] annotations, MediaType mediaType) {
* return getDomainClass(genericType) == Customer.class;
* }
*
* @Override
* public boolean isWriteable(Class<?> type, Type genericType,
* Annotation[] annotations, MediaType mediaType) {
* return isReadable(type, genericType, annotations, mediaType);
* }
*
* @Override
* protected void preReadFrom(Class<Object> type, Type genericType,
* Annotation[] annotations, MediaType mediaType,
* MultivaluedMap<String, String> httpHeaders,
* Unmarshaller unmarshaller) throws JAXBException {
* unmarshaller.setProperty(MarshallerProperties.JSON_VALUE_WRAPPER, "$");
* }
*
* @Override
* protected void preWriteTo(Object object, Class<?> type, Type genericType,
* Annotation[] annotations, MediaType mediaType,
* MultivaluedMap<String, Object> httpHeaders, Marshaller marshaller)
* throws JAXBException {
* marshaller.setProperty(MarshallerProperties.JSON_VALUE_WRAPPER, "$");
* }
*
* }
*
* @since 2.4
*/
@Produces({MediaType.APPLICATION_JSON, MediaType.WILDCARD, "application/x-javascript"})
@Consumes({MediaType.APPLICATION_JSON, MediaType.WILDCARD})
@Provider
public class MOXyJsonProvider implements MessageBodyReader, MessageBodyWriter{
private static final String APPLICATION_XJAVASCRIPT = "application/x-javascript";
private static final String CHARSET = "charset";
private static final QName EMPTY_STRING_QNAME = new QName("");
private static final String JSON = "json";
private static final String PLUS_JSON = "+json";
@Context
protected Providers providers;
private String attributePrefix = null;
private Map>, JAXBContext> contextCache = new HashMap<>();
private boolean formattedOutput = false;
private boolean includeRoot = false;
private boolean marshalEmptyCollections = true;
private Map namespacePrefixMapper;
private char namespaceSeperator = Constants.DOT;
private String valueWrapper;
private boolean wrapperAsArrayName = false;
/**
* The value that will be prepended to all keys that are mapped to an XML
* attribute. By default there is no attribute prefix.
* @see org.eclipse.persistence.jaxb.MarshallerProperties#JSON_ATTRIBUTE_PREFIX
* @see org.eclipse.persistence.jaxb.UnmarshallerProperties#JSON_ATTRIBUTE_PREFIX
*/
public String getAttributePrefix() {
return attributePrefix;
}
/**
* A convenience method to get the domain class (i.e. Customer or Foo, Bar ) from
* the parameter/return type (i.e. Customer , List<Customer> ,
* JAXBElement<Customer> , JAXBElement<? extends Customer> ,
* List<JAXBElement<Customer>> , or
* List<JAXBElement<? extends Customer>>
* List<Foo<Bar>> ).
* @param genericType - The parameter/return type of the JAX-RS operation.
* @return The corresponding domain classes.
*/
protected Set> getDomainClasses(Type genericType) {
if(null == genericType) {
return asSet(Object.class);
}
if(genericType instanceof Class> clazz && genericType != JAXBElement.class) {
if(clazz.isArray()) {
return getDomainClasses(clazz.getComponentType());
}
return asSet(clazz);
} else if(genericType instanceof ParameterizedType) {
Set> result = new LinkedHashSet<>();
result.add((Class>)((ParameterizedType) genericType).getRawType());
Type[] types = ((ParameterizedType) genericType).getActualTypeArguments();
for (Type upperType : types) {
result.addAll(getDomainClasses(upperType));
}
return result;
} else if (genericType instanceof GenericArrayType genericArrayType) {
return getDomainClasses(genericArrayType.getGenericComponentType());
} else if(genericType instanceof WildcardType) {
Set> result = new LinkedHashSet<>();
Type[] upperTypes = ((WildcardType)genericType).getUpperBounds();
if(upperTypes.length > 0){
for (Type upperType : upperTypes) {
result.addAll(getDomainClasses(upperType));
}
} else {
result.add(Object.class);
}
return result;
} else {
return asSet(Object.class);
}
}
private Set> asSet(Class> clazz) {
Set> result = new LinkedHashSet<>();
result.add(clazz);
return result;
}
/**
* Return the JAXBContext that corresponds to the domain class. This
* method does the following:
*
* If an EclipseLink JAXB (MOXy) JAXBContext is available from
* a ContextResolver then use it.
* If an existing JAXBContext was not found in step one, then
* create a new one on the domain class.
*
* @param domainClasses - The domain classes we need a JAXBContext for.
* @param annotations - The annotations corresponding to domain object.
* @param mediaType - The media type for the HTTP entity.
* @param httpHeaders - HTTP headers associated with HTTP entity.
*/
protected JAXBContext getJAXBContext(Set> domainClasses, Annotation[] annotations, MediaType mediaType, MultivaluedMap httpHeaders) throws JAXBException {
JAXBContext jaxbContext = contextCache.get(domainClasses);
if(null != jaxbContext) {
return jaxbContext;
}
synchronized (contextCache) {
jaxbContext = contextCache.get(domainClasses);
if(null != jaxbContext) {
return jaxbContext;
}
ContextResolver resolver = null;
if(null != providers) {
resolver = providers.getContextResolver(JAXBContext.class, mediaType);
}
if (null != resolver && domainClasses.size() == 1) {
jaxbContext = resolver.getContext(domainClasses.iterator().next());
}
if(null == jaxbContext) {
jaxbContext = JAXBContextFactory.createContext(domainClasses.toArray(new Class>[0]), null);
contextCache.put(domainClasses, jaxbContext);
return jaxbContext;
} else if (jaxbContext instanceof org.eclipse.persistence.jaxb.JAXBContext) {
return jaxbContext;
} else {
jaxbContext = JAXBContextFactory.createContext(domainClasses.toArray(new Class>[0]), null);
contextCache.put(domainClasses, jaxbContext);
return jaxbContext;
}
}
}
private JAXBContext getJAXBContext(Class> type, Type genericType, Annotation[] annotations, MediaType mediaType) {
if(null == genericType) {
genericType = type;
}
try {
Set> domainClasses = getDomainClasses(genericType);
return getJAXBContext(domainClasses, annotations, mediaType, null);
} catch(JAXBException e) {
AbstractSessionLog.getLog().logThrowable(SessionLog.WARNING, SessionLog.MOXY, e);
return null;
}
}
/**
* By default the JSON-binding will ignore namespace qualification. If this
* property is set the portion of the key before the namespace separator
* will be used to determine the namespace URI.
* @see org.eclipse.persistence.jaxb.MarshallerProperties#NAMESPACE_PREFIX_MAPPER
* @see org.eclipse.persistence.jaxb.UnmarshallerProperties#JSON_NAMESPACE_PREFIX_MAPPER
*/
public Map getNamespacePrefixMapper() {
return namespacePrefixMapper;
}
/**
* This character (default is '.') separates the prefix from the key name.
* It is only used if namespace qualification has been enabled be setting a
* namespace prefix mapper.
* @see org.eclipse.persistence.jaxb.MarshallerProperties#JSON_NAMESPACE_SEPARATOR
* @see org.eclipse.persistence.jaxb.UnmarshallerProperties#JSON_NAMESPACE_SEPARATOR
*/
public char getNamespaceSeparator() {
return this.namespaceSeperator;
}
/*
* @return -1 since the size of the JSON message is not known.
* @see jakarta.ws.rs.ext.MessageBodyWriter#getSize(java.lang.Object, java.lang.Class, java.lang.reflect.Type, java.lang.annotation.Annotation[], jakarta.ws.rs.core.MediaType)
*/
@Override
public long getSize(Object t, Class> type, Type genericType, Annotation[] annotations, MediaType mediaType) {
return -1;
}
/**
* The key that will correspond to the property mapped with @XmlValue. This
* key will only be used if there are other mapped properties.
* @see org.eclipse.persistence.jaxb.MarshallerProperties#JSON_VALUE_WRAPPER
* @see org.eclipse.persistence.jaxb.UnmarshallerProperties#JSON_VALUE_WRAPPER
*/
public String getValueWrapper() {
return valueWrapper;
}
/**
* @return true if the JSON output should be formatted (default is false).
*/
public boolean isFormattedOutput() {
return formattedOutput;
}
/**
* @return true if the root node is included in the JSON message (default is
* false).
* @see org.eclipse.persistence.jaxb.MarshallerProperties#JSON_INCLUDE_ROOT
* @see org.eclipse.persistence.jaxb.UnmarshallerProperties#JSON_INCLUDE_ROOT
*/
public boolean isIncludeRoot() {
return includeRoot;
}
/**
* If true empty collections will be marshalled as empty arrays, else the
* collection will not be marshalled to JSON (default is true).
* @see org.eclipse.persistence.jaxb.MarshallerProperties#JSON_MARSHAL_EMPTY_COLLECTIONS
*/
public boolean isMarshalEmptyCollections() {
return marshalEmptyCollections;
}
/**
* @return true indicating that MOXyJsonProvider will
* be used for the JSON binding if the media type is of the following
* patterns */json or */*+json, and the type is not assignable from
* any of (or a Collection or JAXBElement of) the following:
*
* byte[]
* java.io.File
* java.io.InputStream
* java.io.Reader
* java.lang.Object
* java.lang.String
* jakarta.activation.DataSource
*
*/
@Override
public boolean isReadable(Class> type, Type genericType, Annotation[] annotations, MediaType mediaType) {
if(!supportsMediaType(mediaType)) {
return false;
} else if(CoreClassConstants.APBYTE == type || CoreClassConstants.STRING == type) {
return false;
} else if(Map.class.isAssignableFrom(type)) {
return false;
} else if(File.class.isAssignableFrom(type)) {
return false;
} else if(DataSource.class.isAssignableFrom(type)) {
return false;
} else if(InputStream.class.isAssignableFrom(type)) {
return false;
} else if(Reader.class.isAssignableFrom(type)) {
return false;
} else if(Object.class == type) {
return false;
} else if(type.isPrimitive()) {
return false;
} else if(type.isArray() && (type.getComponentType().isArray() || type.getComponentType().isPrimitive() || type.getComponentType().getPackage().getName().startsWith("java."))) {
return false;
} else if(JAXBElement.class.isAssignableFrom(type)) {
Set> domainClasses = getDomainClasses(genericType);
for (Class> domainClass : domainClasses) {
if (isReadable(domainClass, null, annotations, mediaType) || String.class == domainClass) {
return true;
}
}
return false;
} else if(Collection.class.isAssignableFrom(type)) {
Set> domainClasses = getDomainClasses(genericType);
for (Class> domainClass : domainClasses) {
if (isReadable(domainClass, null, annotations, mediaType) || String.class == domainClass) {
return true;
}
}
return false;
} else {
return null != getJAXBContext(type, genericType, annotations, mediaType);
}
}
/**
* If true the grouping element will be used as the JSON key.
*
* Example
* Given the following class:
*
* @XmlAccessorType(XmlAccessType.FIELD)
* public class Customer {
*
* @XmlElementWrapper(name="phone-numbers")
* @XmlElement(name="phone-number")
* private {@literal List} phoneNumbers;
*
* }
*
* If the property is set to false (the default) the JSON output will be:
*
* {
* "phone-numbers" : {
* "phone-number" : [ {
* ...
* }, {
* ...
* }]
* }
* }
*
* And if the property is set to true, then the JSON output will be:
*
* {
* "phone-numbers" : [ {
* ...
* }, {
* ...
* }]
* }
*
* @since 2.4.2
* @see org.eclipse.persistence.jaxb.JAXBContextProperties#JSON_WRAPPER_AS_ARRAY_NAME
* @see org.eclipse.persistence.jaxb.MarshallerProperties#JSON_WRAPPER_AS_ARRAY_NAME
* @see org.eclipse.persistence.jaxb.UnmarshallerProperties#JSON_WRAPPER_AS_ARRAY_NAME
*/
public boolean isWrapperAsArrayName() {
return wrapperAsArrayName;
}
/**
* @return true indicating that MOXyJsonProvider will
* be used for the JSON binding if the media type is of the following
* patterns */json or */*+json, and the type is not assignable from
* any of (or a Collection or JAXBElement of) the following:
*
* byte[]
* java.io.File
* java.lang.Object
* java.lang.String
* jakarta.activation.DataSource
* jakarta.ws.rs.core.StreamingOutput
*
*/
@Override
public boolean isWriteable(Class> type, Type genericType, Annotation[] annotations, MediaType mediaType) {
if(type == JSONWithPadding.class && APPLICATION_XJAVASCRIPT.equals(mediaType.toString())) {
return true;
}
if(!supportsMediaType(mediaType)) {
return false;
} else if(CoreClassConstants.APBYTE == type || CoreClassConstants.STRING == type || type.isPrimitive()) {
return false;
} else if(Map.class.isAssignableFrom(type)) {
return false;
} else if(File.class.isAssignableFrom(type)) {
return false;
} else if(DataSource.class.isAssignableFrom(type)) {
return false;
} else if(StreamingOutput.class.isAssignableFrom(type)) {
return false;
} else if(Object.class == type) {
return false;
} else if(type.isPrimitive()) {
return false;
} else if(type.isArray() && (String.class.equals(type.getComponentType()) || type.getComponentType().isPrimitive() || Helper.isPrimitiveWrapper(type.getComponentType()))) {
return true;
} else if(type.isArray() && (type.getComponentType().isArray() || type.getComponentType().isPrimitive() || type.getComponentType().getPackage().getName().startsWith("java."))) {
return false;
} else if(JAXBElement.class.isAssignableFrom(type)) {
Set> domainClasses = getDomainClasses(genericType);
for (Class> domainClass : domainClasses) {
if (isWriteable(domainClass, null, annotations, mediaType) || domainClass == String.class) {
return true;
}
}
return false;
} else if(Collection.class.isAssignableFrom(type)) {
Set> domainClasses = getDomainClasses(genericType);
//special case for List>
//this is quick fix, MOXyJsonProvider should be refactored as stated in issue #459541
if (domainClasses.size() == 3) {
Class>[] domainArray = domainClasses.toArray(new Class>[0]);
if (JAXBElement.class.isAssignableFrom(domainArray[1]) && String.class == domainArray[2]) {
return true;
}
}
for (Class> domainClass : domainClasses) {
if (String.class.equals(domainClass) || domainClass.isPrimitive() || Helper.isPrimitiveWrapper(domainClass)) {
return true;
}
String packageName = domainClass.getPackage().getName();
if(null == packageName || !packageName.startsWith("java.")) {
if (isWriteable(domainClass, null, annotations, mediaType)) {
return true;
}
}
}
return false;
} else {
return null != getJAXBContext(type, genericType, annotations, mediaType);
}
}
/**
* Subclasses of MOXyJsonProvider can override this method to
* customize the instance of Unmarshaller that will be used to
* unmarshal the JSON message in the readFrom call.
* @param type - The Class to be unmarshalled (i.e. Customer or
* List )
* @param genericType - The type of object to be unmarshalled (i.e
* Customer or List<Customer> ).
* @param annotations - The annotations corresponding to domain object.
* @param mediaType - The media type for the HTTP entity.
* @param httpHeaders - HTTP headers associated with HTTP entity.
* @param unmarshaller - The instance of Unmarshaller that will be
* used to unmarshal the JSON message.
* @see org.eclipse.persistence.jaxb.UnmarshallerProperties
*/
protected void preReadFrom(Class type, Type genericType, Annotation[] annotations, MediaType mediaType, MultivaluedMap httpHeaders, Unmarshaller unmarshaller) throws JAXBException {
}
/**
* Subclasses of MOXyJsonProvider can override this method to
* customize the instance of Marshaller that will be used to marshal
* the domain objects to JSON in the writeTo call.
* @param object - The domain object that will be marshalled to JSON.
* @param type - The Class to be marshalled (i.e. Customer or
* List )
* @param genericType - The type of object to be marshalled (i.e
* Customer or List<Customer> ).
* @param annotations - The annotations corresponding to domain object.
* @param mediaType - The media type for the HTTP entity.
* @param httpHeaders - HTTP headers associated with HTTP entity.
* @param marshaller - The instance of Marshaller that will be used
* to marshal the domain object to JSON.
* @see org.eclipse.persistence.jaxb.MarshallerProperties
*/
protected void preWriteTo(Object object, Class> type, Type genericType, Annotation[] annotations, MediaType mediaType, MultivaluedMap httpHeaders, Marshaller marshaller) throws JAXBException {
}
/*
* @see jakarta.ws.rs.ext.MessageBodyReader#readFrom(java.lang.Class, java.lang.reflect.Type, java.lang.annotation.Annotation[], jakarta.ws.rs.core.MediaType, jakarta.ws.rs.core.MultivaluedMap, java.io.InputStream)
*/
@Override
public Object readFrom(Class type, Type genericType, Annotation[] annotations, MediaType mediaType, MultivaluedMap httpHeaders, InputStream entityStream) throws IOException, WebApplicationException {
try {
if(null == genericType) {
genericType = type;
}
Set> domainClasses = getDomainClasses(genericType);
JAXBContext jaxbContext = getJAXBContext(domainClasses, annotations, mediaType, httpHeaders);
SessionLog logger = AbstractSessionLog.getLog();
if (logger.shouldLog(SessionLog.FINE, SessionLog.MOXY)) {
logger.log(SessionLog.FINE, SessionLog.MOXY, "moxy_read_from_moxy_json_provider", new Object[0]);
}
Unmarshaller unmarshaller = jaxbContext.createUnmarshaller();
unmarshaller.setProperty(UnmarshallerProperties.MEDIA_TYPE, MediaType.APPLICATION_JSON);
unmarshaller.setProperty(UnmarshallerProperties.JSON_ATTRIBUTE_PREFIX, attributePrefix);
unmarshaller.setProperty(UnmarshallerProperties.JSON_INCLUDE_ROOT, includeRoot);
unmarshaller.setProperty(UnmarshallerProperties.JSON_NAMESPACE_PREFIX_MAPPER, namespacePrefixMapper);
unmarshaller.setProperty(UnmarshallerProperties.JSON_NAMESPACE_SEPARATOR, namespaceSeperator);
if(null != valueWrapper) {
unmarshaller.setProperty(UnmarshallerProperties.JSON_VALUE_WRAPPER, valueWrapper);
}
unmarshaller.setProperty(UnmarshallerProperties.JSON_WRAPPER_AS_ARRAY_NAME, wrapperAsArrayName);
preReadFrom(type, genericType, annotations, mediaType, httpHeaders, unmarshaller);
StreamSource jsonSource;
Map mediaTypeParameters = null;
if(null != mediaType) {
mediaTypeParameters = mediaType.getParameters();
}
if(null != mediaTypeParameters && mediaTypeParameters.containsKey(CHARSET)) {
String charSet = mediaTypeParameters.get(CHARSET);
Reader entityReader = new InputStreamReader(entityStream, charSet);
jsonSource = new StreamSource(entityReader);
} else {
jsonSource = new StreamSource(entityStream);
}
Class> domainClass = getDomainClass(domainClasses);
JAXBElement> jaxbElement = unmarshaller.unmarshal(jsonSource, domainClass);
if(type.isAssignableFrom(JAXBElement.class)) {
return jaxbElement;
} else {
Object value = jaxbElement.getValue();
if(value instanceof ArrayList) {
if(type.isArray()) {
ArrayList arrayList = (ArrayList) value;
int arrayListSize = arrayList.size();
boolean wrapItemInJAXBElement = wrapItemInJAXBElement(genericType);
Object array;
if(wrapItemInJAXBElement) {
array = Array.newInstance(JAXBElement.class, arrayListSize);
} else {
array = Array.newInstance(domainClass, arrayListSize);
}
for(int x=0; x) value) {
element = handleJAXBElement(element, domainClass, wrapItemInJAXBElement);
containerPolicy.addInto(element, container, null);
}
return container;
}
} else {
return value;
}
}
} catch(UnmarshalException unmarshalException) {
ResponseBuilder builder = Response.status(Status.BAD_REQUEST);
throw new WebApplicationException(unmarshalException, builder.build());
} catch(JAXBException jaxbException) {
throw new WebApplicationException(jaxbException);
} catch(NullPointerException nullPointerException) {
throw new WebApplicationException(JSONException.errorInvalidDocument(nullPointerException));
}
}
/**
* Get first non java class if exists.
*
* @return first domain class or first generic class or just the first class from the list
*/
public Class> getDomainClass(Set> domainClasses) {
if (domainClasses.size() == 1) {
return domainClasses.iterator().next();
}
boolean isStringPresent = false;
for (Class> clazz : domainClasses) {
if (!clazz.getName().startsWith("java.")
&& !clazz.getName().startsWith("javax.")
&& !clazz.getName().startsWith("jakarta.")
&& !java.util.List.class.isAssignableFrom(clazz)) {
return clazz;
} else if (clazz == String.class) {
isStringPresent = true;
}
}
if (isStringPresent) {
return String.class;
}
//handle simple generic case
if (domainClasses.size() >= 2) {
Iterator> it = domainClasses.iterator();
it.next();
return it.next();
}
return domainClasses.iterator().next();
}
private boolean wrapItemInJAXBElement(Type genericType) {
if(genericType == JAXBElement.class) {
return true;
} else if(genericType instanceof GenericArrayType) {
return wrapItemInJAXBElement(((GenericArrayType) genericType).getGenericComponentType());
} else if(genericType instanceof ParameterizedType parameterizedType) {
Type actualType = parameterizedType.getActualTypeArguments()[0];
return wrapItemInJAXBElement(parameterizedType.getOwnerType()) || wrapItemInJAXBElement(parameterizedType.getRawType()) || wrapItemInJAXBElement(actualType);
} else {
return false;
}
}
private Object handleJAXBElement(Object element, Class> domainClass, boolean wrapItemInJAXBElement) {
if(wrapItemInJAXBElement) {
if(element instanceof JAXBElement) {
return element;
} else {
return new JAXBElement(EMPTY_STRING_QNAME, domainClass, element);
}
} else {
return JAXBIntrospector.getValue(element);
}
}
/**
* Specify a value that will be prepended to all keys that are mapped to an
* XML attribute. By default there is no attribute prefix.
* @see org.eclipse.persistence.jaxb.MarshallerProperties#JSON_ATTRIBUTE_PREFIX
* @see org.eclipse.persistence.jaxb.UnmarshallerProperties#JSON_ATTRIBUTE_PREFIX
*/
public void setAttributePrefix(String attributePrefix) {
this.attributePrefix = attributePrefix;
}
/**
* Specify if the JSON output should be formatted (default is false).
* @param formattedOutput - true if the output should be formatted, else
* false.
*/
public void setFormattedOutput(boolean formattedOutput) {
this.formattedOutput = formattedOutput;
}
/**
* Specify if the root node should be included in the JSON message (default
* is false).
* @param includeRoot - true if the message includes the root node, else
* false.
* @see org.eclipse.persistence.jaxb.MarshallerProperties#JSON_INCLUDE_ROOT
* @see org.eclipse.persistence.jaxb.UnmarshallerProperties#JSON_INCLUDE_ROOT
*/
public void setIncludeRoot(boolean includeRoot) {
this.includeRoot = includeRoot;
}
/**
* If true empty collections will be marshalled as empty arrays, else the
* collection will not be marshalled to JSON (default is true).
* @see org.eclipse.persistence.jaxb.MarshallerProperties#JSON_MARSHAL_EMPTY_COLLECTIONS
*/
public void setMarshalEmptyCollections(boolean marshalEmptyCollections) {
this.marshalEmptyCollections = marshalEmptyCollections;
}
/**
* By default the JSON-binding will ignore namespace qualification. If this
* property is set then a prefix corresponding to the namespace URI and a
* namespace separator will be prefixed to the key.
* include it you can specify a Map of namespace URI to prefix.
* @see org.eclipse.persistence.jaxb.MarshallerProperties#NAMESPACE_PREFIX_MAPPER
* @see org.eclipse.persistence.jaxb.UnmarshallerProperties#JSON_NAMESPACE_PREFIX_MAPPER
*/
public void setNamespacePrefixMapper(Map namespacePrefixMapper) {
this.namespacePrefixMapper = namespacePrefixMapper;
}
/**
* This character (default is '.') separates the prefix from the key name.
* It is only used if namespace qualification has been enabled be setting a
* namespace prefix mapper.
* @see org.eclipse.persistence.jaxb.MarshallerProperties#JSON_NAMESPACE_SEPARATOR
* @see org.eclipse.persistence.jaxb.UnmarshallerProperties#JSON_NAMESPACE_SEPARATOR
*/
public void setNamespaceSeparator(char namespaceSeparator) {
this.namespaceSeperator = namespaceSeparator;
}
/**
* If true the grouping element will be used as the JSON key.
*
* Example
* Given the following class:
*
* @XmlAccessorType(XmlAccessType.FIELD)
* public class Customer {
*
* @XmlElementWrapper(name="phone-numbers")
* @XmlElement(name="phone-number")
* private {@literal List} phoneNumbers;
*
* }
*
* If the property is set to false (the default) the JSON output will be:
*
* {
* "phone-numbers" : {
* "phone-number" : [ {
* ...
* }, {
* ...
* }]
* }
* }
*
* And if the property is set to true, then the JSON output will be:
*
* {
* "phone-numbers" : [ {
* ...
* }, {
* ...
* }]
* }
*
* @since 2.4.2
* @see org.eclipse.persistence.jaxb.JAXBContextProperties#JSON_WRAPPER_AS_ARRAY_NAME
* @see org.eclipse.persistence.jaxb.MarshallerProperties#JSON_WRAPPER_AS_ARRAY_NAME
* @see org.eclipse.persistence.jaxb.UnmarshallerProperties#JSON_WRAPPER_AS_ARRAY_NAME
*/
public void setWrapperAsArrayName(boolean wrapperAsArrayName) {
this.wrapperAsArrayName = wrapperAsArrayName;
}
/**
* Specify the key that will correspond to the property mapped with
* {@literal @XmlValue}. This key will only be used if there are other mapped
* properties.
* @see org.eclipse.persistence.jaxb.MarshallerProperties#JSON_VALUE_WRAPPER
* @see org.eclipse.persistence.jaxb.UnmarshallerProperties#JSON_VALUE_WRAPPER
*/
public void setValueWrapper(String valueWrapper) {
this.valueWrapper = valueWrapper;
}
/**
* @return true for all media types of the pattern */json and
* */*+json.
*/
protected boolean supportsMediaType(MediaType mediaType) {
if(null == mediaType) {
return true;
}
String subtype = mediaType.getSubtype();
return subtype.equals(JSON) || subtype.endsWith(PLUS_JSON);
}
/**
* @see jakarta.ws.rs.ext.MessageBodyWriter#writeTo(java.lang.Object, java.lang.Class, java.lang.reflect.Type, java.lang.annotation.Annotation[], jakarta.ws.rs.core.MediaType, jakarta.ws.rs.core.MultivaluedMap, java.io.OutputStream)
*/
@Override
public void writeTo(Object object, Class> type, Type genericType, Annotation[] annotations, MediaType mediaType, MultivaluedMap httpHeaders, OutputStream entityStream) throws IOException, WebApplicationException {
try {
if(null == genericType) {
genericType = type;
}
Set> domainClasses = getDomainClasses(genericType);
JAXBContext jaxbContext = getJAXBContext(domainClasses, annotations, mediaType, httpHeaders);
SessionLog logger = AbstractSessionLog.getLog();
if (logger.shouldLog(SessionLog.FINE, SessionLog.MOXY)) {
logger.log(SessionLog.FINE, SessionLog.MOXY, "moxy_write_to_moxy_json_provider", new Object[0]);
}
Marshaller marshaller = jaxbContext.createMarshaller();
marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, formattedOutput);
marshaller.setProperty(MarshallerProperties.MEDIA_TYPE, MediaType.APPLICATION_JSON);
marshaller.setProperty(MarshallerProperties.JSON_ATTRIBUTE_PREFIX, attributePrefix);
marshaller.setProperty(MarshallerProperties.JSON_INCLUDE_ROOT, includeRoot);
marshaller.setProperty(MarshallerProperties.JSON_MARSHAL_EMPTY_COLLECTIONS, marshalEmptyCollections);
marshaller.setProperty(MarshallerProperties.JSON_NAMESPACE_SEPARATOR, namespaceSeperator);
if(null != valueWrapper) {
marshaller.setProperty(MarshallerProperties.JSON_VALUE_WRAPPER, valueWrapper);
}
marshaller.setProperty(MarshallerProperties.JSON_WRAPPER_AS_ARRAY_NAME, wrapperAsArrayName);
marshaller.setProperty(MarshallerProperties.NAMESPACE_PREFIX_MAPPER, namespacePrefixMapper);
Map mediaTypeParameters = null;
if(null != mediaType) {
mediaTypeParameters = mediaType.getParameters();
}
if(null != mediaTypeParameters && mediaTypeParameters.containsKey(CHARSET)) {
String charSet = mediaTypeParameters.get(CHARSET);
marshaller.setProperty(Marshaller.JAXB_ENCODING, charSet);
}
preWriteTo(object, type, genericType, annotations, mediaType, httpHeaders, marshaller);
if (domainClasses.size() == 1) {
Class> domainClass = domainClasses.iterator().next();
if(!(List.class.isAssignableFrom(type) || type.isArray()) && domainClass.getPackage().getName().startsWith("java.")) {
object = new JAXBElement(new QName((String) marshaller.getProperty(MarshallerProperties.JSON_VALUE_WRAPPER)), domainClass, object);
}
}
marshaller.marshal(object, entityStream);
} catch(JAXBException jaxbException) {
throw new WebApplicationException(jaxbException);
}
}
}