001 /** 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017 package org.apache.camel.component.http; 018 019 import java.io.ByteArrayOutputStream; 020 import java.io.File; 021 import java.io.IOException; 022 import java.io.InputStream; 023 import java.io.ObjectInputStream; 024 import java.io.ObjectOutputStream; 025 import java.io.Serializable; 026 import java.io.UnsupportedEncodingException; 027 import java.util.HashMap; 028 import java.util.Map; 029 030 import org.apache.camel.CamelExchangeException; 031 import org.apache.camel.Exchange; 032 import org.apache.camel.Message; 033 import org.apache.camel.component.file.GenericFile; 034 import org.apache.camel.component.http.helper.GZIPHelper; 035 import org.apache.camel.component.http.helper.HttpHelper; 036 import org.apache.camel.converter.IOConverter; 037 import org.apache.camel.converter.stream.CachedOutputStream; 038 import org.apache.camel.impl.DefaultProducer; 039 import org.apache.camel.spi.HeaderFilterStrategy; 040 import org.apache.camel.util.ExchangeHelper; 041 import org.apache.camel.util.IOHelper; 042 import org.apache.commons.httpclient.Header; 043 import org.apache.commons.httpclient.HttpClient; 044 import org.apache.commons.httpclient.HttpMethod; 045 import org.apache.commons.httpclient.HttpVersion; 046 import org.apache.commons.httpclient.methods.ByteArrayRequestEntity; 047 import org.apache.commons.httpclient.methods.EntityEnclosingMethod; 048 import org.apache.commons.httpclient.methods.FileRequestEntity; 049 import org.apache.commons.httpclient.methods.InputStreamRequestEntity; 050 import org.apache.commons.httpclient.methods.RequestEntity; 051 import org.apache.commons.httpclient.methods.StringRequestEntity; 052 import org.apache.commons.httpclient.params.HttpMethodParams; 053 import org.slf4j.Logger; 054 import org.slf4j.LoggerFactory; 055 056 /** 057 * @version 058 */ 059 public class HttpProducer extends DefaultProducer { 060 private static final transient Logger LOG = LoggerFactory.getLogger(HttpProducer.class); 061 private HttpClient httpClient; 062 private boolean throwException; 063 private boolean transferException; 064 065 public HttpProducer(HttpEndpoint endpoint) { 066 super(endpoint); 067 this.httpClient = endpoint.createHttpClient(); 068 this.throwException = endpoint.isThrowExceptionOnFailure(); 069 this.transferException = endpoint.isTransferException(); 070 } 071 072 public void process(Exchange exchange) throws Exception { 073 if (getEndpoint().isBridgeEndpoint()) { 074 exchange.setProperty(Exchange.SKIP_GZIP_ENCODING, Boolean.TRUE); 075 } 076 HttpMethod method = createMethod(exchange); 077 Message in = exchange.getIn(); 078 String httpProtocolVersion = in.getHeader(Exchange.HTTP_PROTOCOL_VERSION, String.class); 079 if (httpProtocolVersion != null) { 080 // set the HTTP protocol version 081 HttpMethodParams params = method.getParams(); 082 params.setVersion(HttpVersion.parse(httpProtocolVersion)); 083 } 084 085 HeaderFilterStrategy strategy = getEndpoint().getHeaderFilterStrategy(); 086 087 // propagate headers as HTTP headers 088 for (Map.Entry<String, Object> entry : in.getHeaders().entrySet()) { 089 String headerValue = in.getHeader(entry.getKey(), String.class); 090 if (strategy != null && !strategy.applyFilterToCamelHeaders(entry.getKey(), headerValue, exchange)) { 091 method.addRequestHeader(entry.getKey(), headerValue); 092 } 093 } 094 095 // lets store the result in the output message. 096 try { 097 if (LOG.isDebugEnabled()) { 098 LOG.debug("Executing http " + method.getName() + " method: " + method.getURI().toString()); 099 } 100 int responseCode = executeMethod(method); 101 if (LOG.isDebugEnabled()) { 102 LOG.debug("Http responseCode: " + responseCode); 103 } 104 105 if (!throwException) { 106 // if we do not use failed exception then populate response for all response codes 107 populateResponse(exchange, method, in, strategy, responseCode); 108 } else { 109 if (responseCode >= 100 && responseCode < 300) { 110 // only populate response for OK response 111 populateResponse(exchange, method, in, strategy, responseCode); 112 } else { 113 // operation failed so populate exception to throw 114 throw populateHttpOperationFailedException(exchange, method, responseCode); 115 } 116 } 117 } finally { 118 method.releaseConnection(); 119 } 120 } 121 122 @Override 123 public HttpEndpoint getEndpoint() { 124 return (HttpEndpoint) super.getEndpoint(); 125 } 126 127 protected void populateResponse(Exchange exchange, HttpMethod method, Message in, HeaderFilterStrategy strategy, int responseCode) throws IOException, ClassNotFoundException { 128 //We just make the out message is not create when extractResponseBody throws exception, 129 Object response = extractResponseBody(method, exchange); 130 Message answer = exchange.getOut(); 131 132 answer.setHeaders(in.getHeaders()); 133 answer.setHeader(Exchange.HTTP_RESPONSE_CODE, responseCode); 134 answer.setBody(response); 135 136 // propagate HTTP response headers 137 Header[] headers = method.getResponseHeaders(); 138 for (Header header : headers) { 139 String name = header.getName(); 140 String value = header.getValue(); 141 if (name.toLowerCase().equals("content-type")) { 142 name = Exchange.CONTENT_TYPE; 143 } 144 if (strategy != null && !strategy.applyFilterToExternalHeaders(name, value, exchange)) { 145 answer.setHeader(name, value); 146 } 147 } 148 } 149 150 protected Exception populateHttpOperationFailedException(Exchange exchange, HttpMethod method, int responseCode) throws IOException, ClassNotFoundException { 151 Exception answer; 152 153 String uri = method.getURI().toString(); 154 String statusText = method.getStatusLine() != null ? method.getStatusLine().getReasonPhrase() : null; 155 Map<String, String> headers = extractResponseHeaders(method.getResponseHeaders()); 156 157 Object responseBody = extractResponseBody(method, exchange); 158 if (transferException && responseBody != null && responseBody instanceof Exception) { 159 // if the response was a serialized exception then use that 160 return (Exception) responseBody; 161 } 162 163 // make a defensive copy of the response body in the exception so its detached from the cache 164 String copy = null; 165 if (responseBody != null) { 166 copy = exchange.getContext().getTypeConverter().convertTo(String.class, exchange, responseBody); 167 } 168 169 if (responseCode >= 300 && responseCode < 400) { 170 String redirectLocation; 171 Header locationHeader = method.getResponseHeader("location"); 172 if (locationHeader != null) { 173 redirectLocation = locationHeader.getValue(); 174 answer = new HttpOperationFailedException(uri, responseCode, statusText, redirectLocation, headers, copy); 175 } else { 176 // no redirect location 177 answer = new HttpOperationFailedException(uri, responseCode, statusText, null, headers, copy); 178 } 179 } else { 180 // internal server error (error code 500) 181 answer = new HttpOperationFailedException(uri, responseCode, statusText, null, headers, copy); 182 } 183 184 return answer; 185 } 186 187 /** 188 * Strategy when executing the method (calling the remote server). 189 * 190 * @param method the method to execute 191 * @return the response code 192 * @throws IOException can be thrown 193 */ 194 protected int executeMethod(HttpMethod method) throws IOException { 195 return httpClient.executeMethod(method); 196 } 197 198 /** 199 * Extracts the response headers 200 * 201 * @param responseHeaders the headers 202 * @return the extracted headers or <tt>null</tt> if no headers existed 203 */ 204 protected static Map<String, String> extractResponseHeaders(Header[] responseHeaders) { 205 if (responseHeaders == null || responseHeaders.length == 0) { 206 return null; 207 } 208 209 Map<String, String> answer = new HashMap<String, String>(); 210 for (Header header : responseHeaders) { 211 answer.put(header.getName(), header.getValue()); 212 } 213 214 return answer; 215 } 216 217 /** 218 * Extracts the response from the method as a InputStream. 219 * 220 * @param method the method that was executed 221 * @return the response either as a stream, or as a deserialized java object 222 * @throws IOException can be thrown 223 */ 224 protected static Object extractResponseBody(HttpMethod method, Exchange exchange) throws IOException, ClassNotFoundException { 225 InputStream is = method.getResponseBodyAsStream(); 226 if (is == null) { 227 return null; 228 } 229 230 Header header = method.getResponseHeader(Exchange.CONTENT_ENCODING); 231 String contentEncoding = header != null ? header.getValue() : null; 232 233 if (!exchange.getProperty(Exchange.SKIP_GZIP_ENCODING, Boolean.FALSE, Boolean.class)) { 234 is = GZIPHelper.uncompressGzip(contentEncoding, is); 235 } 236 237 // Honor the character encoding 238 String contentType = null; 239 header = method.getResponseHeader("content-type"); 240 if (header != null) { 241 contentType = header.getValue(); 242 // find the charset and set it to the Exchange 243 HttpHelper.setCharsetFromContentType(contentType, exchange); 244 } 245 InputStream response = doExtractResponseBodyAsStream(is, exchange); 246 // if content type is a serialized java object then de-serialize it back to a Java object 247 if (contentType != null && contentType.equals(HttpConstants.CONTENT_TYPE_JAVA_SERIALIZED_OBJECT)) { 248 return HttpHelper.deserializeJavaObjectFromStream(response); 249 } else { 250 return response; 251 } 252 } 253 254 private static InputStream doExtractResponseBodyAsStream(InputStream is, Exchange exchange) throws IOException { 255 // As httpclient is using a AutoCloseInputStream, it will be closed when the connection is closed 256 // we need to cache the stream for it. 257 try { 258 // This CachedOutputStream will not be closed when the exchange is onCompletion 259 CachedOutputStream cos = new CachedOutputStream(exchange, false); 260 IOHelper.copy(is, cos); 261 // When the InputStream is closed, the CachedOutputStream will be closed 262 return cos.getWrappedInputStream(); 263 } finally { 264 IOHelper.close(is, "Extracting response body", LOG); 265 } 266 } 267 268 /** 269 * Creates the HttpMethod to use to call the remote server, either its GET or POST. 270 * 271 * @param exchange the exchange 272 * @return the created method as either GET or POST 273 * @throws CamelExchangeException is thrown if error creating RequestEntity 274 */ 275 protected HttpMethod createMethod(Exchange exchange) throws CamelExchangeException { 276 277 String url = HttpHelper.createURL(exchange, getEndpoint()); 278 279 RequestEntity requestEntity = createRequestEntity(exchange); 280 HttpMethods methodToUse = HttpHelper.createMethod(exchange, getEndpoint(), requestEntity != null); 281 HttpMethod method = methodToUse.createMethod(url); 282 283 // is a query string provided in the endpoint URI or in a header (header overrules endpoint) 284 String queryString = exchange.getIn().getHeader(Exchange.HTTP_QUERY, String.class); 285 if (queryString == null) { 286 queryString = getEndpoint().getHttpUri().getRawQuery(); 287 } 288 if (queryString != null) { 289 // need to make sure the queryString is URI safe 290 method.setQueryString(queryString); 291 } 292 293 if (methodToUse.isEntityEnclosing()) { 294 ((EntityEnclosingMethod) method).setRequestEntity(requestEntity); 295 if (requestEntity != null && requestEntity.getContentType() == null) { 296 if (LOG.isDebugEnabled()) { 297 LOG.debug("No Content-Type provided for URL: " + url + " with exchange: " + exchange); 298 } 299 } 300 } 301 302 return method; 303 } 304 305 /** 306 * Creates a holder object for the data to send to the remote server. 307 * 308 * @param exchange the exchange with the IN message with data to send 309 * @return the data holder 310 * @throws CamelExchangeException is thrown if error creating RequestEntity 311 */ 312 protected RequestEntity createRequestEntity(Exchange exchange) throws CamelExchangeException { 313 Message in = exchange.getIn(); 314 if (in.getBody() == null) { 315 return null; 316 } 317 318 RequestEntity answer = in.getBody(RequestEntity.class); 319 if (answer == null) { 320 try { 321 Object data = in.getBody(); 322 if (data != null) { 323 String contentType = ExchangeHelper.getContentType(exchange); 324 325 if (contentType != null && HttpConstants.CONTENT_TYPE_JAVA_SERIALIZED_OBJECT.equals(contentType)) { 326 // serialized java object 327 Serializable obj = in.getMandatoryBody(Serializable.class); 328 // write object to output stream 329 ByteArrayOutputStream bos = new ByteArrayOutputStream(); 330 HttpHelper.writeObjectToStream(bos, obj); 331 answer = new ByteArrayRequestEntity(bos.toByteArray(), HttpConstants.CONTENT_TYPE_JAVA_SERIALIZED_OBJECT); 332 IOHelper.close(bos); 333 } else if (data instanceof File || data instanceof GenericFile) { 334 // file based (could potentially also be a FTP file etc) 335 File file = in.getBody(File.class); 336 if (file != null) { 337 answer = new FileRequestEntity(file, contentType); 338 } 339 } else if (data instanceof String) { 340 // be a bit careful with String as any type can most likely be converted to String 341 // so we only do an instanceof check and accept String if the body is really a String 342 // do not fallback to use the default charset as it can influence the request 343 // (for example application/x-www-form-urlencoded forms being sent) 344 String charset = IOConverter.getCharsetName(exchange, false); 345 answer = new StringRequestEntity((String) data, contentType, charset); 346 } 347 // fallback as input stream 348 if (answer == null) { 349 // force the body as an input stream since this is the fallback 350 InputStream is = in.getMandatoryBody(InputStream.class); 351 answer = new InputStreamRequestEntity(is, contentType); 352 } 353 } 354 } catch (UnsupportedEncodingException e) { 355 throw new CamelExchangeException("Error creating RequestEntity from message body", exchange, e); 356 } catch (IOException e) { 357 throw new CamelExchangeException("Error serializing message body", exchange, e); 358 } 359 } 360 return answer; 361 } 362 363 public HttpClient getHttpClient() { 364 return httpClient; 365 } 366 367 public void setHttpClient(HttpClient httpClient) { 368 this.httpClient = httpClient; 369 } 370 }