blob: e129eff66ffe77d94c481d830f49fb891f1f42ce [file] [log] [blame]
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.cxf.jaxrs.provider;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.annotation.Annotation;
import java.lang.reflect.Type;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.ResourceBundle;
import java.util.Set;
import java.util.logging.Logger;
import javax.activation.DataHandler;
import javax.activation.DataSource;
import javax.ws.rs.Consumes;
import javax.ws.rs.Produces;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.ext.MessageBodyReader;
import javax.ws.rs.ext.MessageBodyWriter;
import javax.ws.rs.ext.Provider;
import org.apache.cxf.attachment.AttachmentUtil;
import org.apache.cxf.attachment.ByteDataSource;
import org.apache.cxf.common.i18n.BundleUtils;
import org.apache.cxf.common.logging.LogUtils;
import org.apache.cxf.common.util.PrimitiveUtils;
import org.apache.cxf.common.util.PropertyUtils;
import org.apache.cxf.helpers.CastUtils;
import org.apache.cxf.jaxrs.ext.MessageContext;
import org.apache.cxf.jaxrs.ext.multipart.Attachment;
import org.apache.cxf.jaxrs.ext.multipart.ContentDisposition;
import org.apache.cxf.jaxrs.ext.multipart.InputStreamDataSource;
import org.apache.cxf.jaxrs.ext.multipart.Multipart;
import org.apache.cxf.jaxrs.ext.multipart.MultipartBody;
import org.apache.cxf.jaxrs.ext.multipart.MultipartOutputFilter;
import org.apache.cxf.jaxrs.impl.MetadataMap;
import org.apache.cxf.jaxrs.utils.AnnotationUtils;
import org.apache.cxf.jaxrs.utils.ExceptionUtils;
import org.apache.cxf.jaxrs.utils.InjectionUtils;
import org.apache.cxf.jaxrs.utils.JAXRSUtils;
import org.apache.cxf.jaxrs.utils.multipart.AttachmentUtils;
import org.apache.cxf.message.Message;
@Provider
@Consumes({"multipart/related", "multipart/mixed", "multipart/alternative", "multipart/form-data" })
@Produces({"multipart/related", "multipart/mixed", "multipart/alternative", "multipart/form-data" })
public class MultipartProvider extends AbstractConfigurableProvider
implements MessageBodyReader<Object>, MessageBodyWriter<Object> {
private static final String SUPPORT_TYPE_AS_MULTIPART = "support.type.as.multipart";
private static final String SINGLE_PART_IS_COLLECTION = "single.multipart.is.collection";
private static final Logger LOG = LogUtils.getL7dLogger(MultipartProvider.class);
private static final ResourceBundle BUNDLE = BundleUtils.getBundle(MultipartProvider.class);
private static final Set<Class<?>> WELL_KNOWN_MULTIPART_CLASSES = new HashSet<>();
private static final Set<String> MULTIPART_SUBTYPES = new HashSet<>();
static {
WELL_KNOWN_MULTIPART_CLASSES.add(MultipartBody.class);
WELL_KNOWN_MULTIPART_CLASSES.add(Attachment.class);
MULTIPART_SUBTYPES.add("form-data");
MULTIPART_SUBTYPES.add("mixed");
MULTIPART_SUBTYPES.add("related");
MULTIPART_SUBTYPES.add("alternative");
}
@Context
private MessageContext mc;
private String attachmentDir;
private String attachmentThreshold;
private String attachmentMaxSize;
public void setMessageContext(MessageContext context) {
this.mc = context;
}
public void setAttachmentDirectory(String dir) {
attachmentDir = dir;
}
public void setAttachmentThreshold(String threshold) {
attachmentThreshold = threshold;
}
public void setAttachmentMaxSize(String maxSize) {
attachmentMaxSize = maxSize;
}
public boolean isReadable(Class<?> type, Type genericType, Annotation[] annotations,
MediaType mt) {
return isSupported(type, annotations, mt);
}
private boolean isSupported(Class<?> type, Annotation[] anns, MediaType mt) {
return mediaTypeSupported(mt)
&& (WELL_KNOWN_MULTIPART_CLASSES.contains(type)
|| Collection.class.isAssignableFrom(type)
|| Map.class.isAssignableFrom(type) && type != MultivaluedMap.class
|| AnnotationUtils.getAnnotation(anns, Multipart.class) != null
|| PropertyUtils.isTrue(mc.getContextualProperty(SUPPORT_TYPE_AS_MULTIPART)));
}
protected void checkContentLength() {
if (mc != null && isPayloadEmpty(mc.getHttpHeaders())) {
String message = new org.apache.cxf.common.i18n.Message("EMPTY_BODY", BUNDLE).toString();
LOG.warning(message);
throw new WebApplicationException(400);
}
}
public Object readFrom(Class<Object> c, Type t, Annotation[] anns, MediaType mt,
MultivaluedMap<String, String> headers, InputStream is)
throws IOException, WebApplicationException {
checkContentLength();
List<Attachment> infos = AttachmentUtils.getAttachments(
mc, attachmentDir, attachmentThreshold, attachmentMaxSize);
boolean collectionExpected = Collection.class.isAssignableFrom(c);
if (collectionExpected
&& AnnotationUtils.getAnnotation(anns, Multipart.class) == null) {
return getAttachmentCollection(t, infos, anns);
}
if (c.isAssignableFrom(Map.class)) {
Map<String, Object> map = new LinkedHashMap<>(infos.size());
Class<?> actual = getActualType(t, 1);
for (Attachment a : infos) {
map.put(a.getContentType().toString(), fromAttachment(a, actual, actual, anns));
}
return map;
}
if (MultipartBody.class.isAssignableFrom(c)) {
return new MultipartBody(infos);
}
Multipart id = AnnotationUtils.getAnnotation(anns, Multipart.class);
Attachment multipart = AttachmentUtils.getMultipart(id, mt, infos);
if (multipart != null) {
if (collectionExpected
&& !mediaTypeSupported(multipart.getContentType())
&& !PropertyUtils.isTrue(mc.getContextualProperty(SINGLE_PART_IS_COLLECTION))) {
List<Attachment> allMultiparts = AttachmentUtils.getMatchingAttachments(id, infos);
return getAttachmentCollection(t, allMultiparts, anns);
}
return fromAttachment(multipart, c, t, anns);
}
if (id != null && !id.required()) {
/*
* Return default value for a missing optional part
*/
Object defaultValue = null;
if (c.isPrimitive()) {
defaultValue = PrimitiveUtils.read((Class<?>)c == boolean.class ? "false" : "0", c);
}
return defaultValue;
}
throw ExceptionUtils.toBadRequestException(null, null);
}
private Object getAttachmentCollection(Type t, List<Attachment> infos, Annotation[] anns) throws IOException {
Class<?> actual = getActualType(t, 0);
if (Attachment.class.isAssignableFrom(actual)) {
return infos;
}
Collection<Object> objects = new ArrayList<>();
for (Attachment a : infos) {
objects.add(fromAttachment(a, actual, actual, anns));
}
return objects;
}
private Class<?> getActualType(Type type, int pos) {
Class<?> actual = null;
try {
actual = InjectionUtils.getActualType(type, pos);
} catch (Exception ex) {
// ignore;
}
return actual != null && actual != Object.class ? actual : Attachment.class;
}
private <T> Object fromAttachment(Attachment multipart, Class<T> c, Type t, Annotation[] anns)
throws IOException {
if (DataHandler.class.isAssignableFrom(c)) {
return multipart.getDataHandler();
} else if (DataSource.class.isAssignableFrom(c)) {
return multipart.getDataHandler().getDataSource();
} else if (Attachment.class.isAssignableFrom(c)) {
return multipart;
} else {
if (mediaTypeSupported(multipart.getContentType())) {
mc.put("org.apache.cxf.multipart.embedded", true);
mc.put("org.apache.cxf.multipart.embedded.ctype", multipart.getContentType());
mc.put("org.apache.cxf.multipart.embedded.input",
multipart.getDataHandler().getInputStream());
anns = new Annotation[]{};
}
MessageBodyReader<T> r =
mc.getProviders().getMessageBodyReader(c, t, anns, multipart.getContentType());
if (r != null) {
InputStream is = multipart.getDataHandler().getInputStream();
return r.readFrom(c, t, anns, multipart.getContentType(), multipart.getHeaders(),
is);
}
}
return null;
}
private boolean mediaTypeSupported(MediaType mt) {
return "multipart".equals(mt.getType()) && MULTIPART_SUBTYPES.contains(mt.getSubtype());
}
public long getSize(Object t, Class<?> type, Type genericType, Annotation[] annotations,
MediaType mediaType) {
return -1;
}
public boolean isWriteable(Class<?> type, Type genericType, Annotation[] annotations,
MediaType mt) {
return isSupported(type, annotations, mt);
}
public void writeTo(Object obj, Class<?> type, Type genericType, Annotation[] anns, MediaType mt,
MultivaluedMap<String, Object> headers, OutputStream os)
throws IOException, WebApplicationException {
List<Attachment> handlers = convertToDataHandlers(obj, type, genericType, anns, mt);
if (mc.get(AttachmentUtils.OUT_FILTERS) != null) {
List<MultipartOutputFilter> filters = CastUtils.cast((List<?>)mc.get(AttachmentUtils.OUT_FILTERS));
for (MultipartOutputFilter filter : filters) {
filter.filter(handlers);
}
}
mc.put(MultipartBody.OUTBOUND_MESSAGE_ATTACHMENTS, handlers);
handlers.get(0).getDataHandler().writeTo(os);
}
private List<Attachment> convertToDataHandlers(Object obj,
Class<?> type, Type genericType,
Annotation[] anns, MediaType mt) throws IOException {
if (Map.class.isAssignableFrom(obj.getClass())) {
Map<Object, Object> objects = CastUtils.cast((Map<?, ?>)obj);
List<Attachment> handlers = new ArrayList<>(objects.size());
int i = 0;
for (Iterator<Map.Entry<Object, Object>> iter = objects.entrySet().iterator();
iter.hasNext();) {
Map.Entry<Object, Object> entry = iter.next();
Object value = entry.getValue();
Attachment handler = createDataHandler(value, value.getClass(),
new Annotation[]{},
entry.getKey().toString(),
mt.toString(),
i++);
handlers.add(handler);
}
return handlers;
}
String rootMediaType = getRootMediaType(anns, mt);
if (List.class.isAssignableFrom(obj.getClass())) {
return getAttachments((List<?>)obj, rootMediaType);
}
if (MultipartBody.class.isAssignableFrom(type)) {
List<Attachment> atts = ((MultipartBody)obj).getAllAttachments();
// these attachments may have no DataHandlers, but objects only
return getAttachments(atts, rootMediaType);
}
Attachment handler = createDataHandler(obj,
genericType, anns,
rootMediaType, mt.toString(), 1);
return Collections.singletonList(handler);
}
private List<Attachment> getAttachments(List<?> objects, String rootMediaType) throws IOException {
List<Attachment> handlers = new ArrayList<>(objects.size());
for (int i = 0; i < objects.size(); i++) {
Object value = objects.get(i);
Attachment handler = createDataHandler(value,
value.getClass(), new Annotation[]{},
rootMediaType, rootMediaType, i);
handlers.add(handler);
}
return handlers;
}
private <T> Attachment createDataHandler(T obj,
Type genericType,
Annotation[] anns,
String mimeType,
String mainMediaType,
int id) throws IOException {
@SuppressWarnings("unchecked")
Class<T> cls = (Class<T>)obj.getClass();
return createDataHandler(obj, cls, genericType, anns, mimeType, mainMediaType, id);
}
private <T> Attachment createDataHandler(T obj,
Class<T> cls,
Type genericType,
Annotation[] anns,
String mimeType,
String mainMediaType,
int id) throws IOException {
final DataHandler dh;
if (InputStream.class.isAssignableFrom(obj.getClass())) {
dh = createInputStreamDH((InputStream)obj, mimeType);
} else if (DataHandler.class.isAssignableFrom(obj.getClass())) {
dh = (DataHandler)obj;
} else if (DataSource.class.isAssignableFrom(obj.getClass())) {
dh = new DataHandler((DataSource)obj);
} else if (File.class.isAssignableFrom(obj.getClass())) {
File f = (File)obj;
ContentDisposition cd = mainMediaType.startsWith(MediaType.MULTIPART_FORM_DATA)
? new ContentDisposition("form-data;name=file;filename=" + f.getName()) : null;
return new Attachment(AttachmentUtil.BODY_ATTACHMENT_ID, Files.newInputStream(f.toPath()), cd);
} else if (Attachment.class.isAssignableFrom(obj.getClass())) {
Attachment att = (Attachment)obj;
if (att.getObject() == null) {
return att;
}
dh = getHandlerForObject(att.getObject(),
att.getObject().getClass(), new Annotation[]{},
att.getContentType().toString(), id);
return new Attachment(att.getContentId(), dh, att.getHeaders());
} else if (byte[].class.isAssignableFrom(obj.getClass())) {
ByteDataSource source = new ByteDataSource((byte[])obj);
source.setContentType(mimeType);
dh = new DataHandler(source);
} else {
dh = getHandlerForObject(obj, cls, genericType, anns, mimeType);
}
String contentId = getContentId(anns, id);
MultivaluedMap<String, String> headers = new MetadataMap<>();
headers.putSingle("Content-Type", mimeType);
return new Attachment(contentId, dh, headers);
}
private String getContentId(Annotation[] anns, int id) {
Multipart part = AnnotationUtils.getAnnotation(anns, Multipart.class);
if (part != null && !"".equals(part.value())) {
return part.value();
}
return id == 0 ? AttachmentUtil.BODY_ATTACHMENT_ID : Integer.toString(id);
}
private <T> DataHandler getHandlerForObject(T obj,
Class<T> cls, Type genericType,
Annotation[] anns,
String mimeType) {
MediaType mt = JAXRSUtils.toMediaType(mimeType);
mc.put(ProviderFactory.ACTIVE_JAXRS_PROVIDER_KEY, this);
final MessageBodyWriter<T> r;
try {
r = mc.getProviders().getMessageBodyWriter(cls, genericType, anns, mt);
} finally {
mc.put(ProviderFactory.ACTIVE_JAXRS_PROVIDER_KEY, null);
}
if (r == null) {
org.apache.cxf.common.i18n.Message message =
new org.apache.cxf.common.i18n.Message("NO_MSG_WRITER",
BUNDLE,
cls);
LOG.severe(message.toString());
throw ExceptionUtils.toInternalServerErrorException(null, null);
}
return new MessageBodyWriterDataHandler<T>(r, obj, cls, genericType, anns, mt);
}
private <T> DataHandler getHandlerForObject(T obj,
Type genericType,
Annotation[] anns,
String mimeType, int id) {
@SuppressWarnings("unchecked")
Class<T> cls = (Class<T>)obj.getClass();
return getHandlerForObject(obj, cls, genericType, anns, mimeType);
}
private DataHandler createInputStreamDH(InputStream is, String mimeType) {
return new DataHandler(new InputStreamDataSource(is, mimeType));
}
private String getRootMediaType(Annotation[] anns, MediaType mt) {
String mimeType = mt.getParameters().get("type");
if (mimeType != null) {
return mimeType;
}
Multipart id = AnnotationUtils.getAnnotation(anns, Multipart.class);
if (id != null && !MediaType.WILDCARD.equals(id.type())) {
mimeType = id.type();
}
if (mimeType == null) {
if (PropertyUtils.isTrue(mc.getContextualProperty(Message.MTOM_ENABLED))) {
mimeType = "text/xml";
} else {
mimeType = MediaType.APPLICATION_OCTET_STREAM;
}
}
return mimeType;
}
private static class MessageBodyWriterDataHandler<T> extends DataHandler {
private MessageBodyWriter<T> writer;
private T obj;
private Class<T> cls;
private Type genericType;
private Annotation[] anns;
private MediaType contentType;
MessageBodyWriterDataHandler(MessageBodyWriter<T> writer,
T obj,
Class<T> cls,
Type genericType,
Annotation[] anns,
MediaType contentType) {
super(new ByteDataSource("1".getBytes(), contentType.toString()));
this.writer = writer;
this.obj = obj;
this.cls = cls;
this.genericType = genericType;
this.anns = anns;
this.contentType = contentType;
}
@Override
public void writeTo(OutputStream os) {
try {
writer.writeTo(obj, cls, genericType, anns, contentType,
new MetadataMap<String, Object>(), os);
} catch (IOException ex) {
throw ExceptionUtils.toInternalServerErrorException(ex, null);
}
}
@Override
public String getContentType() {
return contentType.toString();
}
// TODO : throw UnsupportedOperationException for all other DataHandler methods
}
}