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.codehaus.jackson.JsonFactory;
050import org.codehaus.jackson.JsonParser;
051import org.nuxeo.ecm.automation.core.util.BlobList;
052import org.nuxeo.ecm.automation.jaxrs.io.InputStreamDataSource;
053import org.nuxeo.ecm.automation.jaxrs.io.SharedFileInputStream;
054import org.nuxeo.ecm.core.api.Blob;
055import org.nuxeo.ecm.core.api.Blobs;
056import org.nuxeo.ecm.core.api.CoreSession;
057import org.nuxeo.ecm.webengine.WebException;
058import org.nuxeo.ecm.webengine.jaxrs.context.RequestContext;
059import org.nuxeo.ecm.webengine.jaxrs.session.SessionFactory;
060import org.nuxeo.runtime.api.Framework;
061
062/**
063 * @author <a href="mailto:bs@nuxeo.com">Bogdan Stefanescu</a>
064 */
065@Provider
066@Consumes({ "multipart/form-data", "multipart/related" })
067public class MultiPartFormRequestReader implements MessageBodyReader<ExecutionRequest> {
068
069    private static final Log log = LogFactory.getLog(MultiPartFormRequestReader.class);
070
071    @Context
072    protected HttpServletRequest request;
073
074    @Context
075    JsonFactory factory;
076
077    public CoreSession getCoreSession() {
078        return SessionFactory.getSession(request);
079    }
080
081    @Override
082    public boolean isReadable(Class<?> arg0, Type arg1, Annotation[] arg2, MediaType arg3) {
083        return ExecutionRequest.class.isAssignableFrom(arg0); // TODO check media type too
084    }
085
086    @Override
087    public ExecutionRequest readFrom(Class<ExecutionRequest> arg0, Type arg1, Annotation[] arg2, MediaType arg3,
088            MultivaluedMap<String, String> headers, InputStream in) throws IOException, WebApplicationException {
089        ExecutionRequest req = null;
090        try {
091            List<String> ctypes = headers.get("Content-Type");
092            String ctype = ctypes.get(0);
093            // we need to copy first the stream into a file otherwise it may
094            // happen that
095            // javax.mail fail to receive some parts - I am not sure why -
096            // perhaps the stream is no more available when javax.mail need it?
097            File tmp = Framework.createTempFile("nx-automation-mp-upload-", ".tmp");
098            FileUtils.copyInputStreamToFile(in, tmp);
099            // get the input from the saved file
100            in = new SharedFileInputStream(tmp);
101            try {
102                MimeMultipart mp = new MimeMultipart(new InputStreamDataSource(in, ctype));
103                BodyPart part = mp.getBodyPart(0); // use content ids
104                InputStream pin = part.getInputStream();
105                JsonParser jp = factory.createJsonParser(pin);
106                req = JsonRequestReader.readRequest(jp, headers, getCoreSession());
107                int cnt = mp.getCount();
108                if (cnt == 2) { // a blob
109                    req.setInput(readBlob(request, mp.getBodyPart(1)));
110                } else if (cnt > 2) { // a blob list
111                    BlobList blobs = new BlobList();
112                    for (int i = 1; i < cnt; i++) {
113                        blobs.add(readBlob(request, mp.getBodyPart(i)));
114                    }
115                    req.setInput(blobs);
116                } else {
117                    log.error("Not all parts received.");
118                    for (int i = 0; i < cnt; i++) {
119                        log.error("Received parts: " + mp.getBodyPart(i).getHeader("Content-ID")[0] + " -> "
120                                + mp.getBodyPart(i).getContentType());
121                    }
122                    throw WebException.newException(
123                            new IllegalStateException("Received only " + cnt + " part in a multipart request"));
124                }
125            } finally {
126                try {
127                    in.close();
128                } catch (IOException e) {
129                    // do nothing
130                }
131                tmp.delete();
132            }
133        } catch (MessagingException | IOException e) {
134            throw WebException.newException("Failed to parse multipart request", e);
135        }
136        return req;
137    }
138
139    public static Blob readBlob(HttpServletRequest request, BodyPart part) throws MessagingException, IOException {
140        String ctype = part.getContentType();
141        String fname = part.getFileName();
142        try {
143            // get back the original filename header bytes and try to decode them using UTF-8
144            // if decoding succeeds, use it as the new filename, otherwise keep the original one
145            byte[] bytes = fname.getBytes("ISO-8859-1");
146            CharsetDecoder dec = Charset.forName("UTF-8").newDecoder();
147            CharBuffer buffer = dec.onUnmappableCharacter(CodingErrorAction.REPORT)
148                                   .onMalformedInput(CodingErrorAction.REPORT)
149                                   .decode(ByteBuffer.wrap(bytes));
150            fname = buffer.toString();
151        } catch (CharacterCodingException e) {
152            // do nothing, keep the original filename
153        }
154
155        InputStream pin = part.getInputStream();
156        final File tmp = Framework.createTempFile("nx-automation-upload-", ".tmp");
157        FileUtils.copyInputStreamToFile(pin, tmp);
158        Blob blob = Blobs.createBlob(tmp, ctype, null, fname);
159        RequestContext.getActiveContext(request).addRequestCleanupHandler(req -> tmp.delete());
160        return blob;
161    }
162
163}