/** * 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.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; import org.apache.cxf.message.MessageUtils; @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; private static final Set<String> MULTIPART_SUBTYPES; static { WELL_KNOWN_MULTIPART_CLASSES = new HashSet<Class<?>>(); WELL_KNOWN_MULTIPART_CLASSES.add(MultipartBody.class); WELL_KNOWN_MULTIPART_CLASSES.add(Attachment.class); MULTIPART_SUBTYPES = new HashSet<>(); 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, genericType, annotations, mt); } private boolean isSupported(Class<?> type, Type genericType, 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 || MessageUtils.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<String, Object>(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()) && !MessageUtils.isTrue(mc.getContextualProperty(SINGLE_PART_IS_COLLECTION))) { List<Attachment> allMultiparts = AttachmentUtils.getMatchingAttachments(id, infos); return getAttachmentCollection(t, allMultiparts, anns); } else { 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 mt.getType().equals("multipart") && 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, genericType, 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; } else { String rootMediaType = getRootMediaType(anns, mt); if (List.class.isAssignableFrom(obj.getClass())) { return getAttachments((List<?>)obj, rootMediaType); } else { 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 { DataHandler dh = null; 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, id); } String contentId = getContentId(anns, id); MultivaluedMap<String, String> headers = new MetadataMap<String, String>(); 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, int id) { MediaType mt = JAXRSUtils.toMediaType(mimeType); mc.put(ProviderFactory.ACTIVE_JAXRS_PROVIDER_KEY, this); MessageBodyWriter<T> r = null; 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, id); } 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 (MessageUtils.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())); 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 } }