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.Serializable; 024 import java.io.UnsupportedEncodingException; 025 import java.net.URI; 026 import java.net.URISyntaxException; 027 import java.util.ArrayList; 028 import java.util.HashMap; 029 import java.util.Iterator; 030 import java.util.List; 031 import java.util.Map; 032 033 import org.apache.camel.CamelExchangeException; 034 import org.apache.camel.Exchange; 035 import org.apache.camel.Message; 036 import org.apache.camel.component.file.GenericFile; 037 import org.apache.camel.component.http.helper.HttpHelper; 038 import org.apache.camel.converter.IOConverter; 039 import org.apache.camel.converter.stream.CachedOutputStream; 040 import org.apache.camel.impl.DefaultProducer; 041 import org.apache.camel.spi.HeaderFilterStrategy; 042 import org.apache.camel.util.ExchangeHelper; 043 import org.apache.camel.util.GZIPHelper; 044 import org.apache.camel.util.IOHelper; 045 import org.apache.camel.util.MessageHelper; 046 import org.apache.camel.util.ObjectHelper; 047 import org.apache.camel.util.URISupport; 048 import org.apache.commons.httpclient.Header; 049 import org.apache.commons.httpclient.HttpClient; 050 import org.apache.commons.httpclient.HttpMethod; 051 import org.apache.commons.httpclient.HttpVersion; 052 import org.apache.commons.httpclient.methods.ByteArrayRequestEntity; 053 import org.apache.commons.httpclient.methods.EntityEnclosingMethod; 054 import org.apache.commons.httpclient.methods.FileRequestEntity; 055 import org.apache.commons.httpclient.methods.InputStreamRequestEntity; 056 import org.apache.commons.httpclient.methods.RequestEntity; 057 import org.apache.commons.httpclient.methods.StringRequestEntity; 058 import org.apache.commons.httpclient.params.HttpMethodParams; 059 import org.slf4j.Logger; 060 import org.slf4j.LoggerFactory; 061 062 /** 063 * @version 064 */ 065 public class HttpProducer extends DefaultProducer { 066 private static final transient Logger LOG = LoggerFactory.getLogger(HttpProducer.class); 067 private HttpClient httpClient; 068 private boolean throwException; 069 private boolean transferException; 070 071 public HttpProducer(HttpEndpoint endpoint) { 072 super(endpoint); 073 this.httpClient = endpoint.createHttpClient(); 074 this.throwException = endpoint.isThrowExceptionOnFailure(); 075 this.transferException = endpoint.isTransferException(); 076 } 077 078 public void process(Exchange exchange) throws Exception { 079 // if we bridge endpoint then we need to skip matching headers with the HTTP_QUERY to avoid sending 080 // duplicated headers to the receiver, so use this skipRequestHeaders as the list of headers to skip 081 Map<String, Object> skipRequestHeaders = null; 082 083 if (getEndpoint().isBridgeEndpoint()) { 084 exchange.setProperty(Exchange.SKIP_GZIP_ENCODING, Boolean.TRUE); 085 String queryString = exchange.getIn().getHeader(Exchange.HTTP_QUERY, String.class); 086 if (queryString != null) { 087 skipRequestHeaders = URISupport.parseQuery(queryString); 088 } 089 } 090 HttpMethod method = createMethod(exchange); 091 Message in = exchange.getIn(); 092 String httpProtocolVersion = in.getHeader(Exchange.HTTP_PROTOCOL_VERSION, String.class); 093 if (httpProtocolVersion != null) { 094 // set the HTTP protocol version 095 HttpMethodParams params = method.getParams(); 096 params.setVersion(HttpVersion.parse(httpProtocolVersion)); 097 } 098 099 HeaderFilterStrategy strategy = getEndpoint().getHeaderFilterStrategy(); 100 101 // propagate headers as HTTP headers 102 for (Map.Entry<String, Object> entry : in.getHeaders().entrySet()) { 103 String key = entry.getKey(); 104 Object headerValue = in.getHeader(key); 105 106 if (headerValue != null) { 107 // use an iterator as there can be multiple values. (must not use a delimiter) 108 final Iterator it = ObjectHelper.createIterator(headerValue, null); 109 110 // the value to add as request header 111 final List<String> values = new ArrayList<String>(); 112 113 // if its a multi value then check each value if we can add it and for multi values they 114 // should be combined into a single value 115 while (it.hasNext()) { 116 String value = exchange.getContext().getTypeConverter().convertTo(String.class, it.next()); 117 118 // we should not add headers for the parameters in the uri if we bridge the endpoint 119 // as then we would duplicate headers on both the endpoint uri, and in HTTP headers as well 120 if (skipRequestHeaders != null && skipRequestHeaders.containsKey(key)) { 121 Object skipValue = skipRequestHeaders.get(key); 122 if (ObjectHelper.equal(skipValue, value)) { 123 continue; 124 } 125 } 126 if (value != null && strategy != null && !strategy.applyFilterToCamelHeaders(key, value, exchange)) { 127 values.add(value); 128 } 129 } 130 131 // add the value(s) as a http request header 132 if (values.size() > 0) { 133 // use the default toString of a ArrayList to create in the form [xxx, yyy] 134 // if multi valued, for a single value, then just output the value as is 135 String s = values.size() > 1 ? values.toString() : values.get(0); 136 method.addRequestHeader(key, s); 137 } 138 } 139 } 140 141 // lets store the result in the output message. 142 try { 143 if (LOG.isDebugEnabled()) { 144 LOG.debug("Executing http {} method: {}", method.getName(), method.getURI().toString()); 145 } 146 int responseCode = executeMethod(method); 147 LOG.debug("Http responseCode: {}", responseCode); 148 149 if (!throwException) { 150 // if we do not use failed exception then populate response for all response codes 151 populateResponse(exchange, method, in, strategy, responseCode); 152 } else { 153 if (responseCode >= 100 && responseCode < 300) { 154 // only populate response for OK response 155 populateResponse(exchange, method, in, strategy, responseCode); 156 } else { 157 // operation failed so populate exception to throw 158 throw populateHttpOperationFailedException(exchange, method, responseCode); 159 } 160 } 161 } finally { 162 method.releaseConnection(); 163 } 164 } 165 166 @Override 167 public HttpEndpoint getEndpoint() { 168 return (HttpEndpoint) super.getEndpoint(); 169 } 170 171 protected void populateResponse(Exchange exchange, HttpMethod method, Message in, HeaderFilterStrategy strategy, int responseCode) throws IOException, ClassNotFoundException { 172 //We just make the out message is not create when extractResponseBody throws exception, 173 Object response = extractResponseBody(method, exchange); 174 Message answer = exchange.getOut(); 175 176 answer.setHeader(Exchange.HTTP_RESPONSE_CODE, responseCode); 177 answer.setBody(response); 178 179 // propagate HTTP response headers 180 Header[] headers = method.getResponseHeaders(); 181 for (Header header : headers) { 182 String name = header.getName(); 183 String value = header.getValue(); 184 if (name.toLowerCase().equals("content-type")) { 185 name = Exchange.CONTENT_TYPE; 186 } 187 // use http helper to extract parameter value as it may contain multiple values 188 Object extracted = HttpHelper.extractHttpParameterValue(value); 189 if (strategy != null && !strategy.applyFilterToExternalHeaders(name, extracted, exchange)) { 190 HttpHelper.appendHeader(answer.getHeaders(), name, extracted); 191 } 192 } 193 194 // preserve headers from in by copying any non existing headers 195 // to avoid overriding existing headers with old values 196 MessageHelper.copyHeaders(exchange.getIn(), answer, false); 197 } 198 199 protected Exception populateHttpOperationFailedException(Exchange exchange, HttpMethod method, int responseCode) throws IOException, ClassNotFoundException { 200 Exception answer; 201 202 String uri = method.getURI().toString(); 203 String statusText = method.getStatusLine() != null ? method.getStatusLine().getReasonPhrase() : null; 204 Map<String, String> headers = extractResponseHeaders(method.getResponseHeaders()); 205 206 Object responseBody = extractResponseBody(method, exchange); 207 if (transferException && responseBody != null && responseBody instanceof Exception) { 208 // if the response was a serialized exception then use that 209 return (Exception) responseBody; 210 } 211 212 // make a defensive copy of the response body in the exception so its detached from the cache 213 String copy = null; 214 if (responseBody != null) { 215 copy = exchange.getContext().getTypeConverter().convertTo(String.class, exchange, responseBody); 216 } 217 218 if (responseCode >= 300 && responseCode < 400) { 219 String redirectLocation; 220 Header locationHeader = method.getResponseHeader("location"); 221 if (locationHeader != null) { 222 redirectLocation = locationHeader.getValue(); 223 answer = new HttpOperationFailedException(uri, responseCode, statusText, redirectLocation, headers, copy); 224 } else { 225 // no redirect location 226 answer = new HttpOperationFailedException(uri, responseCode, statusText, null, headers, copy); 227 } 228 } else { 229 // internal server error (error code 500) 230 answer = new HttpOperationFailedException(uri, responseCode, statusText, null, headers, copy); 231 } 232 233 return answer; 234 } 235 236 /** 237 * Strategy when executing the method (calling the remote server). 238 * 239 * @param method the method to execute 240 * @return the response code 241 * @throws IOException can be thrown 242 */ 243 protected int executeMethod(HttpMethod method) throws IOException { 244 return httpClient.executeMethod(method); 245 } 246 247 /** 248 * Extracts the response headers 249 * 250 * @param responseHeaders the headers 251 * @return the extracted headers or <tt>null</tt> if no headers existed 252 */ 253 protected static Map<String, String> extractResponseHeaders(Header[] responseHeaders) { 254 if (responseHeaders == null || responseHeaders.length == 0) { 255 return null; 256 } 257 258 Map<String, String> answer = new HashMap<String, String>(); 259 for (Header header : responseHeaders) { 260 answer.put(header.getName(), header.getValue()); 261 } 262 263 return answer; 264 } 265 266 /** 267 * Extracts the response from the method as a InputStream. 268 * 269 * @param method the method that was executed 270 * @return the response either as a stream, or as a deserialized java object 271 * @throws IOException can be thrown 272 */ 273 protected static Object extractResponseBody(HttpMethod method, Exchange exchange) throws IOException, ClassNotFoundException { 274 InputStream is = method.getResponseBodyAsStream(); 275 if (is == null) { 276 return null; 277 } 278 279 Header header = method.getResponseHeader(Exchange.CONTENT_ENCODING); 280 String contentEncoding = header != null ? header.getValue() : null; 281 282 if (!exchange.getProperty(Exchange.SKIP_GZIP_ENCODING, Boolean.FALSE, Boolean.class)) { 283 is = GZIPHelper.uncompressGzip(contentEncoding, is); 284 } 285 286 // Honor the character encoding 287 String contentType = null; 288 header = method.getResponseHeader("content-type"); 289 if (header != null) { 290 contentType = header.getValue(); 291 // find the charset and set it to the Exchange 292 HttpHelper.setCharsetFromContentType(contentType, exchange); 293 } 294 InputStream response = doExtractResponseBodyAsStream(is, exchange); 295 // if content type is a serialized java object then de-serialize it back to a Java object 296 if (contentType != null && contentType.equals(HttpConstants.CONTENT_TYPE_JAVA_SERIALIZED_OBJECT)) { 297 return HttpHelper.deserializeJavaObjectFromStream(response); 298 } else { 299 return response; 300 } 301 } 302 303 private static InputStream doExtractResponseBodyAsStream(InputStream is, Exchange exchange) throws IOException { 304 // As httpclient is using a AutoCloseInputStream, it will be closed when the connection is closed 305 // we need to cache the stream for it. 306 try { 307 // This CachedOutputStream will not be closed when the exchange is onCompletion 308 CachedOutputStream cos = new CachedOutputStream(exchange, false); 309 IOHelper.copy(is, cos); 310 // When the InputStream is closed, the CachedOutputStream will be closed 311 return cos.getWrappedInputStream(); 312 } finally { 313 IOHelper.close(is, "Extracting response body", LOG); 314 } 315 } 316 317 /** 318 * Creates the HttpMethod to use to call the remote server, either its GET or POST. 319 * 320 * @param exchange the exchange 321 * @return the created method as either GET or POST 322 * @throws CamelExchangeException is thrown if error creating RequestEntity 323 * @throws URISyntaxException 324 */ 325 @SuppressWarnings("deprecation") 326 protected HttpMethod createMethod(Exchange exchange) throws CamelExchangeException, URISyntaxException { 327 328 String url = HttpHelper.createURL(exchange, getEndpoint()); 329 URI uri = new URI(url); 330 331 RequestEntity requestEntity = createRequestEntity(exchange); 332 HttpMethods methodToUse = HttpHelper.createMethod(exchange, getEndpoint(), requestEntity != null); 333 HttpMethod method = methodToUse.createMethod(url); 334 335 // is a query string provided in the endpoint URI or in a header (header overrules endpoint) 336 String queryString = exchange.getIn().getHeader(Exchange.HTTP_QUERY, String.class); 337 if (queryString == null) { 338 queryString = getEndpoint().getHttpUri().getRawQuery(); 339 } 340 // We should user the query string from the HTTP_URI header 341 if (queryString == null) { 342 queryString = uri.getQuery(); 343 } 344 if (queryString != null) { 345 // need to make sure the queryString is URI safe 346 method.setQueryString(queryString); 347 } 348 349 if (methodToUse.isEntityEnclosing()) { 350 ((EntityEnclosingMethod) method).setRequestEntity(requestEntity); 351 if (requestEntity != null && requestEntity.getContentType() == null) { 352 LOG.debug("No Content-Type provided for URL: {} with exchange: {}", url, exchange); 353 } 354 } 355 356 // there must be a host on the method 357 if (method.getHostConfiguration().getHost() == null) { 358 throw new IllegalArgumentException("Invalid uri: " + url 359 + ". If you are forwarding/bridging http endpoints, then enable the bridgeEndpoint option on the endpoint: " + getEndpoint()); 360 } 361 362 return method; 363 } 364 365 /** 366 * Creates a holder object for the data to send to the remote server. 367 * 368 * @param exchange the exchange with the IN message with data to send 369 * @return the data holder 370 * @throws CamelExchangeException is thrown if error creating RequestEntity 371 */ 372 protected RequestEntity createRequestEntity(Exchange exchange) throws CamelExchangeException { 373 Message in = exchange.getIn(); 374 if (in.getBody() == null) { 375 return null; 376 } 377 378 RequestEntity answer = in.getBody(RequestEntity.class); 379 if (answer == null) { 380 try { 381 Object data = in.getBody(); 382 if (data != null) { 383 String contentType = ExchangeHelper.getContentType(exchange); 384 385 if (contentType != null && HttpConstants.CONTENT_TYPE_JAVA_SERIALIZED_OBJECT.equals(contentType)) { 386 // serialized java object 387 Serializable obj = in.getMandatoryBody(Serializable.class); 388 // write object to output stream 389 ByteArrayOutputStream bos = new ByteArrayOutputStream(); 390 HttpHelper.writeObjectToStream(bos, obj); 391 answer = new ByteArrayRequestEntity(bos.toByteArray(), HttpConstants.CONTENT_TYPE_JAVA_SERIALIZED_OBJECT); 392 IOHelper.close(bos); 393 } else if (data instanceof File || data instanceof GenericFile) { 394 // file based (could potentially also be a FTP file etc) 395 File file = in.getBody(File.class); 396 if (file != null) { 397 answer = new FileRequestEntity(file, contentType); 398 } 399 } else if (data instanceof String) { 400 // be a bit careful with String as any type can most likely be converted to String 401 // so we only do an instanceof check and accept String if the body is really a String 402 // do not fallback to use the default charset as it can influence the request 403 // (for example application/x-www-form-urlencoded forms being sent) 404 String charset = IOConverter.getCharsetName(exchange, false); 405 answer = new StringRequestEntity((String) data, contentType, charset); 406 } 407 // fallback as input stream 408 if (answer == null) { 409 // force the body as an input stream since this is the fallback 410 InputStream is = in.getMandatoryBody(InputStream.class); 411 answer = new InputStreamRequestEntity(is, contentType); 412 } 413 } 414 } catch (UnsupportedEncodingException e) { 415 throw new CamelExchangeException("Error creating RequestEntity from message body", exchange, e); 416 } catch (IOException e) { 417 throw new CamelExchangeException("Error serializing message body", exchange, e); 418 } 419 } 420 return answer; 421 } 422 423 public HttpClient getHttpClient() { 424 return httpClient; 425 } 426 427 public void setHttpClient(HttpClient httpClient) { 428 this.httpClient = httpClient; 429 } 430 }