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 }