001/*
002 * Copyright (c) 2006-2014 Nuxeo SA (http://nuxeo.com/) and others.
003 *
004 * All rights reserved. This program and the accompanying materials
005 * are made available under the terms of the Eclipse Public License v1.0
006 * which accompanies this distribution, and is available at
007 * http://www.eclipse.org/legal/epl-v10.html
008 *
009 * Contributors:
010 *     bstefanescu
011 *     ataillefer
012 */
013package org.nuxeo.ecm.automation.client.jaxrs.impl;
014
015import java.io.IOException;
016import java.nio.charset.UnsupportedCharsetException;
017import java.util.Map;
018
019import javax.mail.internet.MimeMultipart;
020import javax.ws.rs.core.Response;
021
022import java.nio.charset.StandardCharsets;
023import org.apache.http.Header;
024import org.apache.http.HttpEntity;
025import org.apache.http.HttpResponse;
026import org.apache.http.client.HttpClient;
027import org.apache.http.client.methods.HttpGet;
028import org.apache.http.client.methods.HttpPost;
029import org.apache.http.client.methods.HttpRequestBase;
030import org.apache.http.client.methods.HttpUriRequest;
031import org.apache.http.client.protocol.HttpClientContext;
032import org.apache.http.entity.StringEntity;
033import org.apache.http.impl.client.BasicCookieStore;
034import org.apache.http.params.HttpParams;
035import org.apache.http.protocol.BasicHttpContext;
036import org.apache.http.protocol.HttpContext;
037import org.apache.http.util.EntityUtils;
038
039import org.nuxeo.ecm.automation.client.RemoteException;
040import org.nuxeo.ecm.automation.client.jaxrs.spi.Connector;
041import org.nuxeo.ecm.automation.client.jaxrs.spi.Request;
042
043/**
044 * Connector wrapping a {@link HttpClient} instance.
045 *
046 * @author <a href="mailto:bs@nuxeo.com">Bogdan Stefanescu</a>
047 * @author <a href="mailto:ataillefer@nuxeo.com">Antoine Taillefer</a>
048 */
049public class HttpConnector implements Connector {
050
051    protected final HttpClient http;
052
053    /**
054     * Timeout in milliseconds for the socket, connection manager and connection used by {@link #http}.
055     */
056    protected final int httpConnectionTimeout;
057
058    protected final HttpContext ctx;
059
060    protected String basicAuth;
061
062    public HttpConnector(HttpClient http) {
063        this(http, 0);
064    }
065
066    /**
067     * Allows to set a timeout for the HTTP connection to avoid infinite or quite long waiting periods if:
068     * <ul>
069     * <li>Nuxeo is broken or running into an infinite loop</li>
070     * <li>the network doesn't respond at all</li>
071     * </ul>
072     *
073     * @since 5.7
074     */
075    public HttpConnector(HttpClient http, int httpConnectionTimeout) {
076        this(http, new BasicHttpContext(), httpConnectionTimeout);
077    }
078
079    public HttpConnector(HttpClient http, HttpContext ctx) {
080        this(http, ctx, 0);
081    }
082
083    /**
084     * @see {@link HttpConnector(HttpClient, long)}
085     * @since 5.7
086     */
087    public HttpConnector(HttpClient http, HttpContext ctx, int httpConnectionTimeout) {
088        ctx.setAttribute(HttpClientContext.COOKIE_STORE, new BasicCookieStore());
089        this.http = http;
090        this.httpConnectionTimeout = httpConnectionTimeout;
091        this.ctx = ctx;
092    }
093
094    @Override
095    public Object execute(Request request) {
096        HttpRequestBase httpRequest = null;
097        if (request.getMethod() == Request.POST) {
098            HttpPost post = new HttpPost(request.getUrl());
099            Object obj = request.getEntity();
100            if (obj != null) {
101                HttpEntity entity = null;
102                if (request.isMultiPart()) {
103                    entity = new MultipartRequestEntity((MimeMultipart) obj);
104                } else {
105                    try {
106                        entity = new StringEntity(obj.toString(), StandardCharsets.UTF_8);
107                    } catch (UnsupportedCharsetException e) {
108                        // cannot happen
109                        throw new RuntimeException("Cannot encode into UTF-8", e);
110                    }
111                }
112                post.setEntity(entity);
113            }
114            httpRequest = post;
115        } else {
116            httpRequest = new HttpGet(request.getUrl());
117        }
118        try {
119            return execute(request, httpRequest);
120        } catch (IOException e) {
121            throw new RuntimeException("Cannot execute " + request, e);
122        }
123    }
124
125    protected Object execute(Request request, HttpUriRequest httpReq) throws RemoteException, IOException {
126        for (Map.Entry<String, String> entry : request.entrySet()) {
127            httpReq.setHeader(entry.getKey(), entry.getValue());
128        }
129        HttpResponse resp = executeRequestWithTimeout(httpReq);
130        HttpEntity entity = resp.getEntity();
131        try {
132            int status = resp.getStatusLine().getStatusCode();
133            if (entity == null) {
134                if (status < Response.Status.BAD_REQUEST.getStatusCode()) {
135                    return null;
136                }
137                throw new RemoteException(status, "ServerError", "Server Error", "");
138            }
139            Header ctypeHeader = entity.getContentType();
140            if (ctypeHeader == null) { // handle broken responses with no ctype
141                if (status != Response.Status.OK.getStatusCode()) {
142                    // this may happen when login failed
143                    throw new RemoteException(status, "ServerError", "Server Error", "");
144                }
145                return null; // cannot handle responses with no ctype
146            }
147            String ctype = ctypeHeader.getValue();
148            String disp = null;
149            Header[] hdisp = resp.getHeaders("Content-Disposition");
150            if (hdisp != null && hdisp.length > 0) {
151                disp = hdisp[0].getValue();
152            }
153            return request.handleResult(status, ctype, disp, entity.getContent());
154        } finally {
155            // needed to properly release resources and return the connection to the pool
156            EntityUtils.consume(entity);
157        }
158    }
159
160    protected HttpResponse executeRequestWithTimeout(HttpUriRequest httpReq) throws IOException {
161        // Set timeout for the socket, connection manager
162        // and connection itself
163        if (httpConnectionTimeout > 0) {
164            HttpParams httpParams = http.getParams();
165            httpParams.setIntParameter("http.socket.timeout", httpConnectionTimeout);
166            httpParams.setIntParameter("http.connection-manager.timeout", httpConnectionTimeout);
167            httpParams.setIntParameter("http.connection.timeout", httpConnectionTimeout);
168        }
169        return http.execute(httpReq, ctx);
170    }
171
172}