001/*
002 * (C) Copyright 2006-2016 Nuxeo SA (http://nuxeo.com/) and others.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *     http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 *
016 * Contributors:
017 *     bstefanescu
018 */
019package org.nuxeo.ecm.automation.jaxrs.io.operations;
020
021import java.io.File;
022import java.io.IOException;
023import java.io.InputStream;
024import java.lang.annotation.Annotation;
025import java.lang.reflect.Type;
026import java.nio.ByteBuffer;
027import java.nio.CharBuffer;
028import java.nio.charset.CharacterCodingException;
029import java.nio.charset.Charset;
030import java.nio.charset.CharsetDecoder;
031import java.nio.charset.CodingErrorAction;
032import java.util.List;
033
034import javax.mail.BodyPart;
035import javax.mail.MessagingException;
036import javax.mail.internet.MimeMultipart;
037import javax.servlet.http.HttpServletRequest;
038import javax.ws.rs.Consumes;
039import javax.ws.rs.WebApplicationException;
040import javax.ws.rs.core.Context;
041import javax.ws.rs.core.MediaType;
042import javax.ws.rs.core.MultivaluedMap;
043import javax.ws.rs.ext.MessageBodyReader;
044import javax.ws.rs.ext.Provider;
045
046import org.apache.commons.io.FileUtils;
047import org.apache.commons.logging.Log;
048import org.apache.commons.logging.LogFactory;
049import org.nuxeo.ecm.automation.core.util.BlobList;
050import org.nuxeo.ecm.automation.jaxrs.io.InputStreamDataSource;
051import org.nuxeo.ecm.automation.jaxrs.io.SharedFileInputStream;
052import org.nuxeo.ecm.core.api.Blob;
053import org.nuxeo.ecm.core.api.Blobs;
054import org.nuxeo.ecm.core.api.CoreSession;
055import org.nuxeo.ecm.core.api.NuxeoException;
056import org.nuxeo.ecm.platform.web.common.RequestContext;
057import org.nuxeo.ecm.webengine.jaxrs.session.SessionFactory;
058import org.nuxeo.runtime.api.Framework;
059
060import com.fasterxml.jackson.core.JsonFactory;
061import com.fasterxml.jackson.core.JsonParser;
062
063/**
064 * @author <a href="mailto:bs@nuxeo.com">Bogdan Stefanescu</a>
065 */
066@Provider
067@Consumes({ "multipart/form-data", "multipart/related" })
068public class MultiPartFormRequestReader implements MessageBodyReader<ExecutionRequest> {
069
070    private static final Log log = LogFactory.getLog(MultiPartFormRequestReader.class);
071
072    @Context
073    protected HttpServletRequest request;
074
075    @Context
076    JsonFactory factory;
077
078    public CoreSession getCoreSession() {
079        return SessionFactory.getSession(request);
080    }
081
082    @Override
083    public boolean isReadable(Class<?> arg0, Type arg1, Annotation[] arg2, MediaType arg3) {
084        return ExecutionRequest.class.isAssignableFrom(arg0); // TODO check media type too
085    }
086
087    @Override
088    public ExecutionRequest readFrom(Class<ExecutionRequest> arg0, Type arg1, Annotation[] arg2, MediaType arg3,
089            MultivaluedMap<String, String> headers, InputStream in) throws IOException, WebApplicationException {
090        ExecutionRequest req = null;
091        try {
092            List<String> ctypes = headers.get("Content-Type");
093            String ctype = ctypes.get(0);
094            // we need to copy first the stream into a file otherwise it may
095            // happen that
096            // javax.mail fail to receive some parts - I am not sure why -
097            // perhaps the stream is no more available when javax.mail need it?
098            File tmp = Framework.createTempFile("nx-automation-mp-upload-", ".tmp");
099            FileUtils.copyInputStreamToFile(in, tmp);
100            // get the input from the saved file
101            try (InputStream sfin = new SharedFileInputStream(tmp)) {
102                MimeMultipart mp = new MimeMultipart(new InputStreamDataSource(sfin, ctype));
103                BodyPart part = mp.getBodyPart(0); // use content ids
104                try (InputStream pin = part.getInputStream(); //
105                        JsonParser jp = factory.createParser(pin)) {
106                    req = JsonRequestReader.readRequest(jp, headers, getCoreSession());
107                }
108                int cnt = mp.getCount();
109                if (cnt == 2) { // a blob
110                    req.setInput(readBlob(request, mp.getBodyPart(1)));
111                } else if (cnt > 2) { // a blob list
112                    BlobList blobs = new BlobList();
113                    for (int i = 1; i < cnt; i++) {
114                        blobs.add(readBlob(request, mp.getBodyPart(i)));
115                    }
116                    req.setInput(blobs);
117                } else {
118                    log.error("Not all parts received.");
119                    for (int i = 0; i < cnt; i++) {
120                        log.error("Received parts: " + mp.getBodyPart(i).getHeader("Content-ID")[0] + " -> "
121                                + mp.getBodyPart(i).getContentType());
122                    }
123                    throw new NuxeoException(
124                            new IllegalStateException("Received only " + cnt + " part in a multipart request"));
125                }
126            } finally {
127                tmp.delete();
128            }
129        } catch (MessagingException | IOException e) {
130            throw new NuxeoException("Failed to parse multipart request", e);
131        }
132        return req;
133    }
134
135    public static Blob readBlob(HttpServletRequest request, BodyPart part) throws MessagingException, IOException {
136        String ctype = part.getContentType();
137        String fname = part.getFileName();
138        try {
139            // get back the original filename header bytes and try to decode them using UTF-8
140            // if decoding succeeds, use it as the new filename, otherwise keep the original one
141            byte[] bytes = fname.getBytes("ISO-8859-1");
142            CharsetDecoder dec = Charset.forName("UTF-8").newDecoder();
143            CharBuffer buffer = dec.onUnmappableCharacter(CodingErrorAction.REPORT)
144                                   .onMalformedInput(CodingErrorAction.REPORT)
145                                   .decode(ByteBuffer.wrap(bytes));
146            fname = buffer.toString();
147        } catch (CharacterCodingException e) {
148            // do nothing, keep the original filename
149        }
150
151        final File tmp = Framework.createTempFile("nx-automation-upload-", ".tmp");
152        try (InputStream pin = part.getInputStream()) {
153            FileUtils.copyInputStreamToFile(pin, tmp);
154        }
155        Blob blob = Blobs.createBlob(tmp, ctype, null, fname);
156        RequestContext.getActiveContext(request).addRequestCleanupHandler(req -> tmp.delete());
157        return blob;
158    }
159
160}