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.client.jaxrs.spi;
020
021import static org.nuxeo.ecm.automation.client.Constants.CTYPE_AUTOMATION;
022import static org.nuxeo.ecm.automation.client.Constants.CTYPE_ENTITY;
023import static org.nuxeo.ecm.automation.client.Constants.CTYPE_MULTIPART_EMPTY;
024import static org.nuxeo.ecm.automation.client.Constants.CTYPE_MULTIPART_MIXED;
025import static org.nuxeo.ecm.automation.client.Constants.HEADER_CONTENT_DISPOSITION;
026
027import java.io.File;
028import java.io.FileInputStream;
029import java.io.IOException;
030import java.io.InputStream;
031import java.io.UnsupportedEncodingException;
032import java.net.URLDecoder;
033import java.util.HashMap;
034import java.util.regex.Matcher;
035import java.util.regex.Pattern;
036
037import javax.mail.BodyPart;
038import javax.mail.MessagingException;
039import javax.mail.internet.MimeMultipart;
040import javax.ws.rs.core.Response;
041
042import org.apache.http.Header;
043import org.apache.http.HttpHeaders;
044import org.apache.http.protocol.HttpContext;
045import org.nuxeo.ecm.automation.client.RemoteException;
046import org.nuxeo.ecm.automation.client.jaxrs.spi.marshallers.ExceptionMarshaller;
047import org.nuxeo.ecm.automation.client.jaxrs.util.IOUtils;
048import org.nuxeo.ecm.automation.client.jaxrs.util.InputStreamDataSource;
049import org.nuxeo.ecm.automation.client.jaxrs.util.MultipartInput;
050import org.nuxeo.ecm.automation.client.model.Blob;
051import org.nuxeo.ecm.automation.client.model.Blobs;
052import org.nuxeo.ecm.automation.client.model.FileBlob;
053import org.nuxeo.ecm.automation.client.model.StringBlob;
054
055/**
056 * @author <a href="mailto:bs@nuxeo.com">Bogdan Stefanescu</a>
057 */
058public class Request extends HashMap<String, String> {
059
060    public static final int GET = 0;
061
062    public static final int POST = 1;
063
064    private static final long serialVersionUID = 1L;
065
066    protected static Pattern RFC2231_ATTR_PATTERN = Pattern.compile(
067            ";?\\s*filename\\s*\\\\*.*\\*=([^']*)'([^']*)'\\s*([^;]+)\\s*", Pattern.CASE_INSENSITIVE);
068
069    protected static Pattern ATTR_PATTERN = Pattern.compile(";?\\s*filename\\s*=\\s*([^;]+)\\s*",
070            Pattern.CASE_INSENSITIVE);
071
072    protected final int method;
073
074    protected final String url;
075
076    protected final boolean isMultiPart;
077
078    protected Object entity;
079
080    public Request(int method, String url) {
081        this.method = method;
082        this.url = url;
083        isMultiPart = false;
084    }
085
086    public Request(int method, String url, MimeMultipart entity) {
087        this.method = method;
088        this.url = url;
089        this.entity = entity;
090        isMultiPart = true;
091    }
092
093    public Request(int method, String url, String entity) {
094        this.method = method;
095        this.url = url;
096        this.entity = entity;
097        isMultiPart = false;
098    }
099
100    public int getMethod() {
101        return method;
102    }
103
104    public String getUrl() {
105        return url;
106    }
107
108    public Object getEntity() {
109        return entity;
110    }
111
112    public final boolean isMultiPart() {
113        return isMultiPart;
114    }
115
116    public MimeMultipart asMultiPartEntity() {
117        return isMultiPart ? (MimeMultipart) entity : null;
118    }
119
120    public String asStringEntity() {
121        return isMultiPart ? null : (String) entity;
122    }
123
124    /**
125     * Must read the object from the server response and return it or throw a {@link RemoteException} if server sent an
126     * error.
127     */
128    public Object handleResult(int status, Header[] headers, InputStream stream, HttpContext ctx)
129            throws RemoteException, IOException {
130        // TODO kevin: check if it's enough regarding to entity content type
131        String ctype = getHeaderValue(headers, HttpHeaders.CONTENT_TYPE);
132
133        // Specific http status handling
134        if (status >= Response.Status.BAD_REQUEST.getStatusCode()) {
135            handleException(status, ctype, stream);
136        } else if (status == Response.Status.NO_CONTENT.getStatusCode() || stream == null) {
137            if (ctype != null && ctype.toLowerCase().startsWith(CTYPE_MULTIPART_EMPTY)) {
138                // empty entity and content type of nuxeo empty list
139                return new Blobs();
140            }
141            // no content
142            return null;
143        }
144        // Check content type
145        if (ctype == null) {
146            if (status != Response.Status.OK.getStatusCode()) {
147                // this may happen when login failed
148                throw new RemoteException(status, "ServerError", "Server Error", "");
149            }
150            // cannot handle responses with no content type
151            return null;
152        }
153        // Handle result
154        String disp = getHeaderValue(headers, HEADER_CONTENT_DISPOSITION);
155        String lctype = ctype.toLowerCase();
156        if (lctype.startsWith(CTYPE_AUTOMATION)) {
157            return JsonMarshalling.readRegistry(IOUtils.read(stream));
158        } else if (lctype.startsWith(CTYPE_ENTITY)) {
159            String body = IOUtils.read(stream);
160            try {
161                return JsonMarshalling.readEntity(body);
162            } catch (IOException | RuntimeException e) {
163                return readStringBlob(ctype, getFileName(disp), body);
164            }
165        } else if (lctype.startsWith(CTYPE_MULTIPART_MIXED)) { // list of blobs
166            return readBlobs(ctype, stream);
167        } else { // a blob?
168            return readBlob(ctype, getFileName(disp), stream);
169        }
170    }
171
172    public static MultipartInput buildMultipartInput(Object input, String content) throws IOException {
173        MultipartInput mpinput = new MultipartInput();
174        mpinput.setRequest(content);
175        if (input instanceof Blob) {
176            mpinput.setBlob((Blob) input);
177        } else if (input instanceof Blobs) {
178            mpinput.setBlobs((Blobs) input);
179        } else {
180            throw new IllegalArgumentException("Unsupported binary input object: " + input);
181        }
182        return mpinput;
183    }
184
185    protected static Blobs readBlobs(String ctype, InputStream in) throws IOException {
186        Blobs files = new Blobs();
187        // save the stream to a temporary file
188        File file = IOUtils.copyToTempFile(in);
189        try (FileInputStream fin = new FileInputStream(file)) {
190            MimeMultipart mp = new MimeMultipart(new InputStreamDataSource(fin, ctype));
191            int size = mp.getCount();
192            for (int i = 0; i < size; i++) {
193                BodyPart part = mp.getBodyPart(i);
194                String fname = part.getFileName();
195                files.add(readBlob(part.getContentType(), fname, part.getInputStream()));
196            }
197        } catch (MessagingException e) {
198            throw new IOException(e);
199        } finally {
200            file.delete();
201        }
202        return files;
203    }
204
205    protected static Blob readBlob(String ctype, String fileName, InputStream in) throws IOException {
206        File file = IOUtils.copyToTempFile(in);
207        FileBlob blob = new FileBlob(file);
208        blob.setMimeType(ctype);
209        if (fileName != null) {
210            blob.setFileName(fileName);
211        }
212        return blob;
213    }
214
215    protected static Blob readStringBlob(String ctype, String fileName, String content) {
216        return new StringBlob(fileName, content, ctype);
217    }
218
219    protected static String getFileName(String ctype) {
220        if (ctype == null) {
221            return null;
222        }
223
224        Matcher m = RFC2231_ATTR_PATTERN.matcher(ctype);
225        if (m.find()) {
226            try {
227                return URLDecoder.decode(m.group(3), m.group(1));
228            } catch (UnsupportedEncodingException e) {
229                throw new RuntimeException(e);
230            }
231        }
232        m = ATTR_PATTERN.matcher(ctype);
233        if (m.find()) {
234            return m.group(1);
235        }
236        return null;
237    }
238
239    protected void handleException(int status, String ctype, InputStream stream) throws RemoteException {
240        if (stream == null) {
241            throw new RemoteException(status, "ServerError", "Server Error", "");
242        }
243        String content;
244        try {
245            content = IOUtils.read(stream);
246        } catch (IOException e) {
247            // typically: org.apache.http.ConnectionClosedException: Premature end of chunk coded message body:
248            // closing chunk expected
249            throw new RemoteException(status, "ServerError", "Server Error", "");
250        }
251        if (CTYPE_ENTITY.equalsIgnoreCase(ctype)) {
252            try {
253                throw ExceptionMarshaller.readException(content);
254            } catch (IOException t) {
255                // JSON decoding error in the payload
256                throw new RemoteException(status, "ServerError", "Server Error", content);
257            }
258        } else {
259            // no JSON payload
260            throw new RemoteException(status, "ServerError", "Server Error", content);
261        }
262    }
263
264    public static String getHeaderValue(Header[] headers, String name) {
265        for (Header header : headers) {
266            if (header.getName().equalsIgnoreCase(name)) {
267                return header.getValue();
268            }
269        }
270        return null;
271    }
272
273}