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