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