View Javadoc

1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   * http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.chemistry.opencmis.client.bindings.spi.http;
20  
21  import static org.apache.chemistry.opencmis.commons.impl.CollectionsHelper.isNotEmpty;
22  
23  import java.io.BufferedOutputStream;
24  import java.io.ByteArrayInputStream;
25  import java.io.IOException;
26  import java.io.InputStream;
27  import java.io.OutputStream;
28  import java.math.BigInteger;
29  import java.net.Socket;
30  import java.util.ArrayList;
31  import java.util.HashMap;
32  import java.util.List;
33  import java.util.Map;
34  import java.util.zip.GZIPOutputStream;
35  
36  import javax.net.ssl.HostnameVerifier;
37  import javax.net.ssl.SSLException;
38  import javax.net.ssl.SSLSocket;
39  
40  import org.apache.chemistry.opencmis.client.bindings.impl.ClientVersion;
41  import org.apache.chemistry.opencmis.client.bindings.impl.CmisBindingsHelper;
42  import org.apache.chemistry.opencmis.client.bindings.spi.BindingSession;
43  import org.apache.chemistry.opencmis.commons.SessionParameter;
44  import org.apache.chemistry.opencmis.commons.exceptions.CmisConnectionException;
45  import org.apache.chemistry.opencmis.commons.exceptions.CmisRuntimeException;
46  import org.apache.chemistry.opencmis.commons.impl.IOUtils;
47  import org.apache.chemistry.opencmis.commons.impl.UrlBuilder;
48  import org.apache.chemistry.opencmis.commons.spi.AuthenticationProvider;
49  import org.apache.http.Header;
50  import org.apache.http.HttpEntity;
51  import org.apache.http.HttpResponse;
52  import org.apache.http.HttpVersion;
53  import org.apache.http.client.HttpClient;
54  import org.apache.http.client.methods.HttpDelete;
55  import org.apache.http.client.methods.HttpEntityEnclosingRequestBase;
56  import org.apache.http.client.methods.HttpGet;
57  import org.apache.http.client.methods.HttpPost;
58  import org.apache.http.client.methods.HttpPut;
59  import org.apache.http.client.methods.HttpRequestBase;
60  import org.apache.http.conn.ssl.X509HostnameVerifier;
61  import org.apache.http.entity.AbstractHttpEntity;
62  import org.apache.http.impl.client.DefaultHttpClient;
63  import org.apache.http.params.BasicHttpParams;
64  import org.apache.http.params.HttpConnectionParams;
65  import org.apache.http.params.HttpParams;
66  import org.apache.http.params.HttpProtocolParams;
67  import org.slf4j.Logger;
68  import org.slf4j.LoggerFactory;
69  
70  /**
71   * A {@link HttpInvoker} that uses The Apache HTTP client.
72   */
73  public abstract class AbstractApacheClientHttpInvoker implements HttpInvoker {
74  
75      protected static final Logger LOG = LoggerFactory.getLogger(AbstractApacheClientHttpInvoker.class);
76  
77      protected static final String HTTP_CLIENT = "org.apache.chemistry.opencmis.client.bindings.spi.http.ApacheClientHttpInvoker.httpClient";
78      protected static final int BUFFER_SIZE = 2 * 1024 * 1024;
79  
80      @Override
81      public Response invokeGET(UrlBuilder url, BindingSession session) {
82          return invoke(url, "GET", null, null, null, session, null, null);
83      }
84  
85      @Override
86      public Response invokeGET(UrlBuilder url, BindingSession session, BigInteger offset, BigInteger length) {
87          return invoke(url, "GET", null, null, null, session, offset, length);
88      }
89  
90      @Override
91      public Response invokePOST(UrlBuilder url, String contentType, Output writer, BindingSession session) {
92          return invoke(url, "POST", contentType, null, writer, session, null, null);
93      }
94  
95      @Override
96      public Response invokePUT(UrlBuilder url, String contentType, Map<String, String> headers, Output writer,
97              BindingSession session) {
98          return invoke(url, "PUT", contentType, headers, writer, session, null, null);
99      }
100 
101     @Override
102     public Response invokeDELETE(UrlBuilder url, BindingSession session) {
103         return invoke(url, "DELETE", null, null, null, session, null, null);
104     }
105 
106     protected Response invoke(UrlBuilder url, String method, String contentType, Map<String, String> headers,
107             final Output writer, final BindingSession session, BigInteger offset, BigInteger length) {
108         int respCode = -1;
109 
110         try {
111             // log before connect
112             if (LOG.isDebugEnabled()) {
113                 LOG.debug("Session {}: {} {}", session.getSessionId(), method, url);
114             }
115 
116             // get HTTP client object from session
117             DefaultHttpClient httpclient = (DefaultHttpClient) session.get(HTTP_CLIENT);
118             if (httpclient == null) {
119                 session.writeLock();
120                 try {
121                     httpclient = (DefaultHttpClient) session.get(HTTP_CLIENT);
122                     if (httpclient == null) {
123                         httpclient = createHttpClient(url, session);
124                         session.put(HTTP_CLIENT, httpclient, true);
125                     }
126                 } finally {
127                     session.writeUnlock();
128                 }
129             }
130 
131             HttpRequestBase request = null;
132 
133             if ("GET".equals(method)) {
134                 request = new HttpGet(url.toString());
135             } else if ("POST".equals(method)) {
136                 request = new HttpPost(url.toString());
137             } else if ("PUT".equals(method)) {
138                 request = new HttpPut(url.toString());
139             } else if ("DELETE".equals(method)) {
140                 request = new HttpDelete(url.toString());
141             } else {
142                 throw new CmisRuntimeException("Invalid HTTP method!");
143             }
144 
145             // set content type
146             if (contentType != null) {
147                 request.setHeader("Content-Type", contentType);
148             }
149             // set other headers
150             if (headers != null) {
151                 for (Map.Entry<String, String> header : headers.entrySet()) {
152                     request.addHeader(header.getKey(), header.getValue());
153                 }
154             }
155 
156             // authenticate
157             AuthenticationProvider authProvider = CmisBindingsHelper.getAuthenticationProvider(session);
158             if (authProvider != null) {
159                 Map<String, List<String>> httpHeaders = authProvider.getHTTPHeaders(url.toString());
160                 if (httpHeaders != null) {
161                     for (Map.Entry<String, List<String>> header : httpHeaders.entrySet()) {
162                         if (header.getKey() != null && isNotEmpty(header.getValue())) {
163                             String key = header.getKey();
164                             if (key.equalsIgnoreCase("user-agent")) {
165                                 request.setHeader("User-Agent", header.getValue().get(0));
166                             } else {
167                                 for (String value : header.getValue()) {
168                                     if (value != null) {
169                                         request.addHeader(key, value);
170                                     }
171                                 }
172                             }
173                         }
174                     }
175                 }
176             }
177 
178             // range
179             if ((offset != null) || (length != null)) {
180                 StringBuilder sb = new StringBuilder("bytes=");
181 
182                 if ((offset == null) || (offset.signum() == -1)) {
183                     offset = BigInteger.ZERO;
184                 }
185 
186                 sb.append(offset.toString());
187                 sb.append('-');
188 
189                 if ((length != null) && (length.signum() == 1)) {
190                     sb.append(offset.add(length.subtract(BigInteger.ONE)).toString());
191                 }
192 
193                 request.setHeader("Range", sb.toString());
194             }
195 
196             // compression
197             Object compression = session.get(SessionParameter.COMPRESSION);
198             if ((compression != null) && Boolean.parseBoolean(compression.toString())) {
199                 request.setHeader("Accept-Encoding", "gzip,deflate");
200             }
201 
202             // locale
203             if (session.get(CmisBindingsHelper.ACCEPT_LANGUAGE) instanceof String) {
204                 request.setHeader("Accept-Language", session.get(CmisBindingsHelper.ACCEPT_LANGUAGE).toString());
205             }
206 
207             // send data
208             if (writer != null) {
209                 Object clientCompression = session.get(SessionParameter.CLIENT_COMPRESSION);
210                 final boolean clientCompressionFlag = (clientCompression != null)
211                         && Boolean.parseBoolean(clientCompression.toString());
212                 if (clientCompressionFlag) {
213                     request.setHeader("Content-Encoding", "gzip");
214                 }
215 
216                 AbstractHttpEntity streamEntity = new AbstractHttpEntity() {
217                     @Override
218                     public boolean isChunked() {
219                         return true;
220                     }
221 
222                     @Override
223                     public boolean isRepeatable() {
224                         return false;
225                     }
226 
227                     @Override
228                     public long getContentLength() {
229                         return -1;
230                     }
231 
232                     @Override
233                     public boolean isStreaming() {
234                         return false;
235                     }
236 
237                     @Override
238                     public InputStream getContent() throws IOException {
239                         throw new UnsupportedOperationException();
240                     }
241 
242                     @Override
243                     public void writeTo(final OutputStream outstream) throws IOException {
244                         OutputStream connOut = null;
245 
246                         if (clientCompressionFlag) {
247                             connOut = new GZIPOutputStream(outstream, 4096);
248                         } else {
249                             connOut = outstream;
250                         }
251 
252                         OutputStream out = new BufferedOutputStream(connOut, BUFFER_SIZE);
253                         try {
254                             writer.write(out);
255                         } catch (IOException ioe) {
256                             throw ioe;
257                         } catch (Exception e) {
258                             throw new IOException(e);
259                         }
260                         out.flush();
261 
262                         if (connOut instanceof GZIPOutputStream) {
263                             ((GZIPOutputStream) connOut).finish();
264                         }
265                     }
266                 };
267                 ((HttpEntityEnclosingRequestBase) request).setEntity(streamEntity);
268             }
269 
270             // connect
271             HttpResponse response = httpclient.execute(request);
272             HttpEntity entity = response.getEntity();
273 
274             // get stream, if present
275             respCode = response.getStatusLine().getStatusCode();
276             InputStream inputStream = null;
277             InputStream errorStream = null;
278 
279             if (respCode == 200 || respCode == 201 || respCode == 203 || respCode == 206) {
280                 if (entity != null) {
281                     inputStream = entity.getContent();
282                 } else {
283                     inputStream = new ByteArrayInputStream(new byte[0]);
284                 }
285             } else {
286                 if (entity != null) {
287                     errorStream = entity.getContent();
288                 } else {
289                     errorStream = new ByteArrayInputStream(new byte[0]);
290                 }
291             }
292 
293             // collect headers
294             Map<String, List<String>> responseHeaders = new HashMap<String, List<String>>();
295             for (Header header : response.getAllHeaders()) {
296                 List<String> values = responseHeaders.get(header.getName());
297                 if (values == null) {
298                     values = new ArrayList<String>();
299                     responseHeaders.put(header.getName(), values);
300                 }
301                 values.add(header.getValue());
302             }
303 
304             // log after connect
305             if (LOG.isTraceEnabled()) {
306                 LOG.trace("Session {}: {} {} > Headers: {}", session.getSessionId(), method, url,
307                         responseHeaders.toString());
308             }
309 
310             // forward response HTTP headers
311             if (authProvider != null) {
312                 authProvider.putResponseHeaders(url.toString(), respCode, responseHeaders);
313             }
314 
315             // get the response
316             return new Response(respCode, response.getStatusLine().getReasonPhrase(), responseHeaders, inputStream,
317                     errorStream);
318         } catch (Exception e) {
319             throw new CmisConnectionException(url.toString(), respCode, e);
320         }
321     }
322 
323     /**
324      * Creates default params for the Apache HTTP Client.
325      */
326     protected HttpParams createDefaultHttpParams(BindingSession session) {
327         HttpParams params = new BasicHttpParams();
328 
329         HttpProtocolParams.setVersion(params, HttpVersion.HTTP_1_1);
330         HttpProtocolParams.setUserAgent(params,
331                 (String) session.get(SessionParameter.USER_AGENT, ClientVersion.OPENCMIS_USER_AGENT));
332         HttpProtocolParams.setContentCharset(params, IOUtils.UTF8);
333         HttpProtocolParams.setUseExpectContinue(params, true);
334 
335         HttpConnectionParams.setStaleCheckingEnabled(params, true);
336 
337         int connectTimeout = session.get(SessionParameter.CONNECT_TIMEOUT, -1);
338         if (connectTimeout >= 0) {
339             HttpConnectionParams.setConnectionTimeout(params, connectTimeout);
340         }
341 
342         int readTimeout = session.get(SessionParameter.READ_TIMEOUT, -1);
343         if (readTimeout >= 0) {
344             HttpConnectionParams.setSoTimeout(params, readTimeout);
345         }
346 
347         return params;
348     }
349 
350     /**
351      * Verifies a hostname with the given verifier.
352      */
353     protected void verify(HostnameVerifier verifier, String host, SSLSocket sslSocket) throws IOException {
354         try {
355             if (verifier instanceof X509HostnameVerifier) {
356                 ((X509HostnameVerifier) verifier).verify(host, sslSocket);
357             } else {
358                 if (!verifier.verify(host, sslSocket.getSession())) {
359                     throw new SSLException("Hostname in certificate didn't match: <" + host + ">");
360                 }
361             }
362         } catch (IOException ioe) {
363             closeSocket(sslSocket);
364             throw ioe;
365         }
366     }
367 
368     /**
369      * Closes the given socket and ignores exceptions.
370      */
371     protected void closeSocket(Socket socket) {
372         try {
373             socket.close();
374         } catch (IOException ioe) {
375             // ignore
376         }
377     }
378 
379     /**
380      * Creates the {@link HttpClient} instance.
381      */
382     protected abstract DefaultHttpClient createHttpClient(UrlBuilder url, BindingSession session);
383 }