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.webengine.jaxrs.context.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            in = new SharedFileInputStream(tmp);
102            try {
103                MimeMultipart mp = new MimeMultipart(new InputStreamDataSource(in, ctype));
104                BodyPart part = mp.getBodyPart(0); // use content ids
105                InputStream pin = part.getInputStream();
106                try (JsonParser jp = factory.createParser(pin)) {
107                    req = JsonRequestReader.readRequest(jp, headers, getCoreSession());
108                }
109                int cnt = mp.getCount();
110                if (cnt == 2) { // a blob
111                    req.setInput(readBlob(request, mp.getBodyPart(1)));
112                } else if (cnt > 2) { // a blob list
113                    BlobList blobs = new BlobList();
114                    for (int i = 1; i < cnt; i++) {
115                        blobs.add(readBlob(request, mp.getBodyPart(i)));
116                    }
117                    req.setInput(blobs);
118                } else {
119                    log.error("Not all parts received.");
120                    for (int i = 0; i < cnt; i++) {
121                        log.error("Received parts: " + mp.getBodyPart(i).getHeader("Content-ID")[0] + " -> "
122                                + mp.getBodyPart(i).getContentType());
123                    }
124                    throw new NuxeoException(
125                            new IllegalStateException("Received only " + cnt + " part in a multipart request"));
126                }
127            } finally {
128                try {
129                    in.close();
130                } catch (IOException e) {
131                    // do nothing
132                }
133                tmp.delete();
134            }
135        } catch (MessagingException | IOException e) {
136            throw new NuxeoException("Failed to parse multipart request", e);
137        }
138        return req;
139    }
140
141    public static Blob readBlob(HttpServletRequest request, BodyPart part) throws MessagingException, IOException {
142        String ctype = part.getContentType();
143        String fname = part.getFileName();
144        try {
145            // get back the original filename header bytes and try to decode them using UTF-8
146            // if decoding succeeds, use it as the new filename, otherwise keep the original one
147            byte[] bytes = fname.getBytes("ISO-8859-1");
148            CharsetDecoder dec = Charset.forName("UTF-8").newDecoder();
149            CharBuffer buffer = dec.onUnmappableCharacter(CodingErrorAction.REPORT)
150                                   .onMalformedInput(CodingErrorAction.REPORT)
151                                   .decode(ByteBuffer.wrap(bytes));
152            fname = buffer.toString();
153        } catch (CharacterCodingException e) {
154            // do nothing, keep the original filename
155        }
156
157        InputStream pin = part.getInputStream();
158        final File tmp = Framework.createTempFile("nx-automation-upload-", ".tmp");
159        FileUtils.copyInputStreamToFile(pin, tmp);
160        Blob blob = Blobs.createBlob(tmp, ctype, null, fname);
161        RequestContext.getActiveContext(request).addRequestCleanupHandler(req -> tmp.delete());
162        return blob;
163    }
164
165}