| /* |
| * Copyright (c) 2012, 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: |
| // 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.localization.JAXBLocalization; |
| 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; |
| |
| /** |
| * <p>This is an implementation of <i>MessageBodyReader</i>/<i>MessageBodyWriter |
| * </i> that can be used to enable EclipseLink JAXB (MOXy) as the JSON |
| * provider.</p> |
| * <p> |
| * <b>Supported Media Type Patterns</b> |
| * <ul> |
| * <li>*/json (i.e. application/json and text/json)</li> |
| * <li>*/*+json</li> |
| * </ul> |
| * |
| * <p>Below are some different usage options.</p> |
| * |
| * <b>Option #1 - <i>MOXyJsonProvider</i> Default Behavior</b> |
| * <p>You can use the <i>Application</i> class to specify that |
| * <i>MOXyJsonProvider</i> should be used with your JAX-RS application.</p> |
| * <pre> |
| * 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; |
| * } |
| * |
| * } |
| * </pre> |
| * |
| * <b>Option #2 - Customize <i>MOXyJsonProvider</i></b> |
| * <p>You can use the <i>Application</i> class to specify a configured instance |
| * of <i>MOXyJsonProvider</i> should be used with your JAX-RS application.</p> |
| * <pre> |
| * 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; |
| * } |
| * |
| * } |
| * </pre> |
| * <b>Option #3 - Extend MOXyJsonProvider</b> |
| * <p>You can use MOXyJsonProvider for creating your own |
| * <i>MessageBodyReader</i>/<i>MessageBodyWriter</i>.</p> |
| * <pre> |
| * 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, "$"); |
| * } |
| * |
| * } |
| * </pre> |
| * @since 2.4 |
| */ |
| @Produces({MediaType.APPLICATION_JSON, MediaType.WILDCARD, "application/x-javascript"}) |
| @Consumes({MediaType.APPLICATION_JSON, MediaType.WILDCARD}) |
| @Provider |
| public class MOXyJsonProvider implements MessageBodyReader<Object>, MessageBodyWriter<Object>{ |
| |
| 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<Set<Class<?>>, JAXBContext> contextCache = new HashMap<Set<Class<?>>, JAXBContext>(); |
| private boolean formattedOutput = false; |
| private boolean includeRoot = false; |
| private boolean marshalEmptyCollections = true; |
| private Map<String, String> 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. <i>Customer</i> or <i>Foo, Bar</i>) from |
| * the parameter/return type (i.e. <i>Customer</i>, <i>List<Customer></i>, |
| * <i>JAXBElement<Customer></i>, <i>JAXBElement<? extends Customer></i>, |
| * <i>List<JAXBElement<Customer>></i>, or |
| * <i>List<JAXBElement<? extends Customer>></i> |
| * <i>List<Foo<Bar>></i>). |
| * @param genericType - The parameter/return type of the JAX-RS operation. |
| * @return The corresponding domain classes. |
| */ |
| protected Set<Class<?>> getDomainClasses(Type genericType) { |
| if(null == genericType) { |
| return asSet(Object.class); |
| } |
| if(genericType instanceof Class<?> && genericType != JAXBElement.class) { |
| Class<?> clazz = (Class<?>) genericType; |
| if(clazz.isArray()) { |
| return getDomainClasses(clazz.getComponentType()); |
| } |
| return asSet(clazz); |
| } else if(genericType instanceof ParameterizedType) { |
| Set<Class<?>> result = new LinkedHashSet<Class<?>>(); |
| result.add((Class<?>)((ParameterizedType) genericType).getRawType()); |
| Type[] types = ((ParameterizedType) genericType).getActualTypeArguments(); |
| if(types.length > 0){ |
| for (Type upperType : types) { |
| result.addAll(getDomainClasses(upperType)); |
| } |
| } |
| return result; |
| } else if (genericType instanceof GenericArrayType) { |
| GenericArrayType genericArrayType = (GenericArrayType) genericType; |
| return getDomainClasses(genericArrayType.getGenericComponentType()); |
| } else if(genericType instanceof WildcardType) { |
| Set<Class<?>> result = new LinkedHashSet<Class<?>>(); |
| 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<Class<?>> asSet(Class<?> clazz) { |
| Set<Class<?>> result = new LinkedHashSet<>(); |
| result.add(clazz); |
| return result; |
| } |
| |
| /** |
| * Return the <i>JAXBContext</i> that corresponds to the domain class. This |
| * method does the following: |
| * <ol> |
| * <li>If an EclipseLink JAXB (MOXy) <i>JAXBContext</i> is available from |
| * a <i>ContextResolver</i> then use it.</li> |
| * <li>If an existing <i>JAXBContext</i> was not found in step one, then |
| * create a new one on the domain class.</li> |
| * </ol> |
| * @param domainClasses - The domain classes we need a <i>JAXBContext</i> 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<Class<?>> domainClasses, Annotation[] annotations, MediaType mediaType, MultivaluedMap<String, ?> 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<JAXBContext> 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<Class<?>> 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<String, String> 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 <i>MOXyJsonProvider</i> 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: |
| * <ul> |
| * <li>byte[]</li> |
| * <li>java.io.File</li> |
| * <li>java.io.InputStream</li> |
| * <li>java.io.Reader</li> |
| * <li>java.lang.Object</li> |
| * <li>java.lang.String</li> |
| * <li>jakarta.activation.DataSource</li> |
| * </ul> |
| */ |
| @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<Class<?>> 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<Class<?>> 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. |
| * |
| * <p><b>Example</b></p> |
| * <p>Given the following class:</p> |
| * <pre> |
| * @XmlAccessorType(XmlAccessType.FIELD) |
| * public class Customer { |
| * |
| * @XmlElementWrapper(name="phone-numbers") |
| * @XmlElement(name="phone-number") |
| * private {@literal List<PhoneNumber>} phoneNumbers; |
| * |
| * } |
| * </pre> |
| * <p>If the property is set to false (the default) the JSON output will be:</p> |
| * <pre> |
| * { |
| * "phone-numbers" : { |
| * "phone-number" : [ { |
| * ... |
| * }, { |
| * ... |
| * }] |
| * } |
| * } |
| * </pre> |
| * <p>And if the property is set to true, then the JSON output will be:</p> |
| * <pre> |
| * { |
| * "phone-numbers" : [ { |
| * ... |
| * }, { |
| * ... |
| * }] |
| * } |
| * </pre> |
| * @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 <i>MOXyJsonProvider</i> 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: |
| * <ul> |
| * <li>byte[]</li> |
| * <li>java.io.File</li> |
| * <li>java.lang.Object</li> |
| * <li>java.lang.String</li> |
| * <li>jakarta.activation.DataSource</li> |
| * <li>jakarta.ws.rs.core.StreamingOutput</li> |
| * </ul> |
| */ |
| @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<Class<?>> 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<Class<?>> domainClasses = getDomainClasses(genericType); |
| |
| //special case for List<JAXBElement<String>> |
| //this is quick fix, MOXyJsonProvider should be refactored as stated in issue #459541 |
| if (domainClasses.size() == 3) { |
| Class<?>[] domainArray = domainClasses.toArray(new Class<?>[domainClasses.size()]); |
| 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 <i>MOXyJsonProvider</i> can override this method to |
| * customize the instance of <i>Unmarshaller</i> that will be used to |
| * unmarshal the JSON message in the readFrom call. |
| * @param type - The Class to be unmarshalled (i.e. <i>Customer</i> or |
| * <i>List</i>) |
| * @param genericType - The type of object to be unmarshalled (i.e |
| * <i>Customer</i> or <i>List<Customer></i>). |
| * @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 <i>Unmarshaller</i> that will be |
| * used to unmarshal the JSON message. |
| * @see org.eclipse.persistence.jaxb.UnmarshallerProperties |
| */ |
| protected void preReadFrom(Class<Object> type, Type genericType, Annotation[] annotations, MediaType mediaType, MultivaluedMap<String, String> httpHeaders, Unmarshaller unmarshaller) throws JAXBException { |
| } |
| |
| /** |
| * Subclasses of <i>MOXyJsonProvider</i> can override this method to |
| * customize the instance of <i>Marshaller</i> 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. <i>Customer</i> or |
| * <i>List</i>) |
| * @param genericType - The type of object to be marshalled (i.e |
| * <i>Customer</i> or <i>List<Customer></i>). |
| * @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 <i>Marshaller</i> 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<String, Object> 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<Object> type, Type genericType, Annotation[] annotations, MediaType mediaType, MultivaluedMap<String, String> httpHeaders, InputStream entityStream) throws IOException, WebApplicationException { |
| try { |
| if(null == genericType) { |
| genericType = type; |
| } |
| |
| Set<Class<?>> 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<String, String> 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<Object> arrayList = (ArrayList<Object>) 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<arrayListSize; x++) { |
| Object element = handleJAXBElement(arrayList.get(x), domainClass, wrapItemInJAXBElement); |
| Array.set(array, x, element); |
| } |
| return array; |
| } else { |
| ContainerPolicy containerPolicy; |
| if(type.isAssignableFrom(List.class) || type.isAssignableFrom(ArrayList.class) || type.isAssignableFrom(Collection.class)) { |
| containerPolicy = new CollectionContainerPolicy(ArrayList.class); |
| } else if(type.isAssignableFrom(Set.class)) { |
| containerPolicy = new CollectionContainerPolicy(HashSet.class); |
| } else if(type.isAssignableFrom(Deque.class) || type.isAssignableFrom(Queue.class)) { |
| containerPolicy = new CollectionContainerPolicy(LinkedList.class); |
| } else if(type.isAssignableFrom(NavigableSet.class) || type.isAssignableFrom(SortedSet.class)) { |
| containerPolicy = new CollectionContainerPolicy(TreeSet.class); |
| } else { |
| containerPolicy = new CollectionContainerPolicy(type); |
| } |
| Object container = containerPolicy.containerInstance(); |
| boolean wrapItemInJAXBElement = wrapItemInJAXBElement(genericType); |
| for(Object element : (Collection<Object>) 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<Class<?>> 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<Class<?>> 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 parameterizedType = (ParameterizedType) genericType; |
| 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<String, String> 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. |
| * |
| * <p><b>Example</b></p> |
| * <p>Given the following class:</p> |
| * <pre> |
| * @XmlAccessorType(XmlAccessType.FIELD) |
| * public class Customer { |
| * |
| * @XmlElementWrapper(name="phone-numbers") |
| * @XmlElement(name="phone-number") |
| * private {@literal List<PhoneNumber>} phoneNumbers; |
| * |
| * } |
| * </pre> |
| * <p>If the property is set to false (the default) the JSON output will be:</p> |
| * <pre> |
| * { |
| * "phone-numbers" : { |
| * "phone-number" : [ { |
| * ... |
| * }, { |
| * ... |
| * }] |
| * } |
| * } |
| * </pre> |
| * <p>And if the property is set to true, then the JSON output will be:</p> |
| * <pre> |
| * { |
| * "phone-numbers" : [ { |
| * ... |
| * }, { |
| * ... |
| * }] |
| * } |
| * </pre> |
| * @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<String, Object> httpHeaders, OutputStream entityStream) throws IOException, WebApplicationException { |
| try { |
| if(null == genericType) { |
| genericType = type; |
| } |
| |
| Set<Class<?>> 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<String, String> 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); |
| } |
| } |
| } |