001/*
002 * (C) Copyright 2015-2018 Nuxeo (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 *      Thibaud Arguillere <targuillere@nuxeo.com>
018 *      Vladimir Pasquier <vpasquier@nuxeo.com>
019 *      Ricardo Dias <rdias@nuxeo.com>
020 */
021package org.nuxeo.ecm.automation.features;
022
023import java.io.IOException;
024import java.io.UnsupportedEncodingException;
025import java.nio.charset.Charset;
026import java.util.HashMap;
027import java.util.List;
028import java.util.Map;
029
030import javax.ws.rs.core.HttpHeaders;
031import javax.ws.rs.core.MediaType;
032import javax.ws.rs.core.MultivaluedMap;
033
034import org.apache.commons.lang3.StringUtils;
035import org.nuxeo.ecm.automation.context.ContextHelper;
036import org.nuxeo.ecm.core.api.Blob;
037import org.nuxeo.ecm.core.api.Blobs;
038import org.nuxeo.ecm.core.api.NuxeoException;
039import org.nuxeo.ecm.core.api.impl.blob.StringBlob;
040
041import com.fasterxml.jackson.databind.ObjectMapper;
042import com.sun.jersey.api.client.Client;
043import com.sun.jersey.api.client.ClientResponse;
044import com.sun.jersey.api.client.WebResource;
045import com.sun.jersey.api.client.config.ClientConfig;
046import com.sun.jersey.api.client.config.DefaultClientConfig;
047import com.sun.jersey.api.client.filter.HTTPBasicAuthFilter;
048import com.sun.jersey.core.util.Base64;
049import com.sun.jersey.core.util.MultivaluedMapImpl;
050import com.sun.jersey.multipart.MultiPart;
051import com.sun.jersey.multipart.impl.MultiPartWriter;
052
053/**
054 * @since 7.3
055 */
056public class HTTPHelper implements ContextHelper {
057
058    protected static volatile ObjectMapper mapper = new ObjectMapper();
059
060    private static final Integer TIMEOUT = 1000 * 60 * 5; // 5min
061
062    private static final String HTTP_CONTENT_DISPOSITION = "Content-Disposition";
063
064    public Blob call(String username, String password, String requestType, String path) throws IOException {
065        return call(username, password, requestType, path, null, null, null, null);
066    }
067
068    public Blob call(String username, String password, String requestType, String path, Map<String, String> headers)
069            throws IOException {
070        return call(username, password, requestType, path, null, null, null, headers);
071    }
072
073    public Blob call(String username, String password, String requestType, String path, MultiPart mp)
074            throws IOException {
075        return call(username, password, requestType, path, null, null, mp, null);
076    }
077
078    public Blob call(String username, String password, String requestType, String path, MultiPart mp,
079            Map<String, String> headers) throws IOException {
080        return call(username, password, requestType, path, null, null, mp, headers);
081    }
082
083    public Blob call(String username, String password, String requestType, String path,
084            MultivaluedMap<String, String> queryParams) throws IOException {
085        return call(username, password, requestType, path, null, queryParams, null, null);
086    }
087
088    public Blob call(String username, String password, String requestType, String path, Object data)
089            throws IOException {
090        return call(username, password, requestType, path, data, null, null, null);
091    }
092
093    public Blob call(String username, String password, String requestType, String path, Object data,
094            Map<String, String> headers) throws IOException {
095        return call(username, password, requestType, path, data, null, null, headers);
096    }
097
098    public Blob call(String username, String password, String requestType, String url, Object data,
099            MultivaluedMap<String, String> queryParams, MultiPart mp, Map<String, String> headers) throws IOException {
100        ClientConfig config = new DefaultClientConfig();
101        config.getClasses().add(MultiPartWriter.class);
102        Client client = Client.create(config);
103        client.setConnectTimeout(TIMEOUT);
104        client.setReadTimeout(TIMEOUT);
105        if (username != null && password != null) {
106            client.addFilter(new HTTPBasicAuthFilter(username, password));
107        }
108
109        WebResource wr = client.resource(url);
110
111        if (queryParams != null && !queryParams.isEmpty()) {
112            wr = wr.queryParams(queryParams);
113        }
114        WebResource.Builder builder;
115        builder = wr.accept(MediaType.APPLICATION_JSON);
116        if (mp != null) {
117            builder = wr.type(MediaType.MULTIPART_FORM_DATA_TYPE);
118        }
119
120        // Adding some headers if needed
121        if (headers != null && !headers.isEmpty()) {
122            for (String headerKey : headers.keySet()) {
123                builder.header(headerKey, headers.get(headerKey));
124            }
125        }
126        ClientResponse response;
127        try {
128            switch (requestType) {
129            case "HEAD":
130            case "GET":
131                response = builder.get(ClientResponse.class);
132                break;
133            case "POST":
134                if (mp != null) {
135                    response = builder.post(ClientResponse.class, mp);
136                } else {
137                    response = builder.post(ClientResponse.class, data);
138                }
139                break;
140            case "PUT":
141                if (mp != null) {
142                    response = builder.put(ClientResponse.class, mp);
143                } else {
144                    response = builder.put(ClientResponse.class, data);
145                }
146                break;
147            case "DELETE":
148                response = builder.delete(ClientResponse.class, data);
149                break;
150            default:
151                throw new NuxeoException("Unknown request type: " + requestType);
152            }
153        } catch (Exception e) {
154            throw new RuntimeException(e);
155        }
156        if (response.getStatus() >= 200 && response.getStatus() < 300) {
157            return Blobs.createBlob(response.getEntityInputStream());
158        } else {
159            return new StringBlob(response.getStatusInfo() != null ? response.getStatusInfo().toString() : "error");
160        }
161    }
162
163    /**
164     * @since 8.4
165     */
166    public Blob get(String url, Map<String, Object> options) throws IOException {
167        return invoke("GET", url, null, null, options);
168    }
169
170    /**
171     * @since 8.4
172     */
173    public Blob post(String url, Object data, Map<String, Object> options) throws IOException {
174        return invoke("POST", url, data, null, options);
175    }
176
177    /**
178     * @since 8.4
179     */
180    public Blob post(String url, MultiPart multiPart, Map<String, Object> options) throws IOException {
181        return invoke("POST", url, null, multiPart, options);
182    }
183
184    /**
185     * @since 8.4
186     */
187    public Blob put(String url, Object data, Map<String, Object> options) throws IOException {
188        return invoke("PUT", url, data, null, options);
189    }
190
191    /**
192     * @since 8.4
193     */
194    public Blob put(String url, MultiPart multiPart, Map<String, Object> options) throws IOException {
195        return invoke("PUT", url, null, multiPart, options);
196    }
197
198    /**
199     * @since 8.4
200     */
201    public Blob delete(String url, Object data, Map<String, Object> options) throws IOException {
202        return invoke("DELETE", url, data, null, options);
203    }
204
205    private Blob invoke(String requestType, String url, Object data, MultiPart multipart, Map<String, Object> options)
206            throws IOException {
207        MultivaluedMap<String, String> queryParams = getQueryParameters(options);
208        Map<String, String> headers = getHeaderParameters(options);
209
210        ClientConfig config = new DefaultClientConfig();
211        config.getClasses().add(MultiPartWriter.class);
212        Client client = Client.create(config);
213        client.setConnectTimeout(TIMEOUT);
214
215        WebResource wr = client.resource(url);
216
217        if (queryParams != null && !queryParams.isEmpty()) {
218            wr = wr.queryParams(queryParams);
219        }
220        WebResource.Builder builder;
221        builder = wr.accept(MediaType.APPLICATION_JSON);
222        if (multipart != null) {
223            builder = wr.type(MediaType.MULTIPART_FORM_DATA_TYPE);
224        }
225
226        // Adding some headers if needed
227        if (headers != null && !headers.isEmpty()) {
228            for (String headerKey : headers.keySet()) {
229                builder.header(headerKey, headers.get(headerKey));
230            }
231        }
232        ClientResponse response;
233        try {
234            switch (requestType) {
235            case "HEAD":
236            case "GET":
237                response = builder.get(ClientResponse.class);
238                break;
239            case "POST":
240                if (multipart != null) {
241                    response = builder.post(ClientResponse.class, multipart);
242                } else {
243                    response = builder.post(ClientResponse.class, data);
244                }
245                break;
246            case "PUT":
247                if (multipart != null) {
248                    response = builder.put(ClientResponse.class, multipart);
249                } else {
250                    response = builder.put(ClientResponse.class, data);
251                }
252                break;
253            case "DELETE":
254                response = builder.delete(ClientResponse.class, data);
255                break;
256            default:
257                throw new NuxeoException("Unknown request type: " + requestType);
258            }
259        } catch (Exception e) {
260            throw new RuntimeException(e);
261        }
262        if (response.getStatus() >= 200 && response.getStatus() < 300) {
263            return setUpBlob(response, url);
264        } else {
265            return new StringBlob(response.getStatusInfo() != null ? response.getStatusInfo().toString() : "error");
266        }
267    }
268
269    private Map<String, String> getHeaderParameters(Map<String, Object> options) {
270        if (options != null) {
271            Map<String, String> headers = new HashMap<>();
272
273            Map<String, String> authorization = (Map<String, String>) options.get("auth");
274            if (authorization != null) {
275                String method = authorization.get("method");
276                switch (method) {
277                case "basic":
278                    Map<String, String> header = basicAuthentication(authorization.get("username"),
279                            authorization.get("password"));
280                    headers.putAll(header);
281                    break;
282                default:
283                    break;
284                }
285            }
286
287            Map<String, String> headersOptions = (Map<String, String>) options.get("headers");
288            if (headersOptions != null) {
289                headers.putAll(headersOptions);
290            }
291
292            return headers;
293        }
294        return null;
295    }
296
297    private MultivaluedMap<String, String> getQueryParameters(Map<String, Object> options) {
298        if (options != null) {
299            Map<String, List<String>> params = (Map<String, List<String>>) options.get("params");
300            if (params != null) {
301                MultivaluedMap<String, String> queryParams = new MultivaluedMapImpl();
302                for (String key : params.keySet()) {
303                    queryParams.put(key, params.get(key));
304                }
305                return queryParams;
306            }
307        }
308        return null;
309    }
310
311    private Blob setUpBlob(ClientResponse response, String url) throws IOException {
312        MultivaluedMap<String, String> headers = response.getHeaders();
313        String disposition = headers.getFirst(HTTP_CONTENT_DISPOSITION);
314
315        String filename = "";
316        if (disposition != null) {
317            // extracts file name from header field
318            int index = disposition.indexOf("filename=");
319            if (index > -1) {
320                filename = disposition.substring(index + 9);
321            }
322        } else {
323            // extracts file name from URL
324            filename = url.substring(url.lastIndexOf("/") + 1, url.length());
325        }
326
327        Blob resultBlob = Blobs.createBlob(response.getEntityInputStream());
328        if (!StringUtils.isEmpty(filename)) {
329            resultBlob.setFilename(filename);
330        }
331
332        String encoding = headers.getFirst(HttpHeaders.CONTENT_ENCODING);
333        if (encoding != null) {
334            resultBlob.setEncoding(encoding);
335        }
336
337        MediaType contentType = response.getType();
338        if (contentType != null) {
339            resultBlob.setMimeType(contentType.getType());
340        }
341
342        return resultBlob;
343    }
344
345    private Map<String, String> basicAuthentication(String username, String password) {
346
347        if (username == null || password == null) {
348            return null;
349        }
350
351        Map<String, String> authenticationHeader;
352        try {
353            final byte[] prefix = (username + ":").getBytes(Charset.forName("iso-8859-1"));
354            final byte[] usernamePassword = new byte[prefix.length + password.getBytes().length];
355
356            System.arraycopy(prefix, 0, usernamePassword, 0, prefix.length);
357            System.arraycopy(password.getBytes(), 0, usernamePassword, prefix.length, password.getBytes().length);
358
359            String authentication = "Basic " + new String(Base64.encode(usernamePassword), "ASCII");
360
361            authenticationHeader = new HashMap<>();
362            authenticationHeader.put(HttpHeaders.AUTHORIZATION, authentication);
363
364        } catch (UnsupportedEncodingException ex) {
365            // This should never occur
366            throw new RuntimeException(ex);
367        }
368        return authenticationHeader;
369    }
370}