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