/** * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.apache.camel.component.http; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.Serializable; import java.io.UnsupportedEncodingException; import java.net.URI; import java.net.URISyntaxException; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Map.Entry; import org.apache.camel.CamelExchangeException; import org.apache.camel.Exchange; import org.apache.camel.Message; import org.apache.camel.component.file.GenericFile; import org.apache.camel.converter.stream.CachedOutputStream; import org.apache.camel.http.common.HttpConstants; import org.apache.camel.http.common.HttpHelper; import org.apache.camel.http.common.HttpOperationFailedException; import org.apache.camel.http.common.HttpProtocolHeaderFilterStrategy; import org.apache.camel.impl.DefaultProducer; import org.apache.camel.spi.HeaderFilterStrategy; import org.apache.camel.util.ExchangeHelper; import org.apache.camel.util.GZIPHelper; import org.apache.camel.util.IOHelper; import org.apache.camel.util.MessageHelper; import org.apache.camel.util.ObjectHelper; import org.apache.camel.util.URISupport; import org.apache.camel.util.UnsafeUriCharactersEncoder; import org.apache.commons.httpclient.Header; import org.apache.commons.httpclient.HttpClient; import org.apache.commons.httpclient.HttpMethod; import org.apache.commons.httpclient.HttpVersion; import org.apache.commons.httpclient.cookie.CookiePolicy; import org.apache.commons.httpclient.methods.ByteArrayRequestEntity; import org.apache.commons.httpclient.methods.EntityEnclosingMethod; import org.apache.commons.httpclient.methods.FileRequestEntity; import org.apache.commons.httpclient.methods.InputStreamRequestEntity; import org.apache.commons.httpclient.methods.RequestEntity; import org.apache.commons.httpclient.methods.StringRequestEntity; import org.apache.commons.httpclient.params.HttpMethodParams; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** */ public class HttpProducer extends DefaultProducer { private static final Logger LOG = LoggerFactory.getLogger(HttpProducer.class); private HttpClient httpClient; private boolean throwException; private boolean transferException; private HeaderFilterStrategy httpProtocolHeaderFilterStrategy = new HttpProtocolHeaderFilterStrategy(); public HttpProducer(HttpEndpoint endpoint) { super(endpoint); this.httpClient = endpoint.createHttpClient(); this.throwException = endpoint.isThrowExceptionOnFailure(); this.transferException = endpoint.isTransferException(); } public void process(Exchange exchange) throws Exception { // if we bridge endpoint then we need to skip matching headers with the HTTP_QUERY to avoid sending // duplicated headers to the receiver, so use this skipRequestHeaders as the list of headers to skip Map<String, Object> skipRequestHeaders = null; if (getEndpoint().isBridgeEndpoint()) { exchange.setProperty(Exchange.SKIP_GZIP_ENCODING, Boolean.TRUE); String queryString = exchange.getIn().getHeader(Exchange.HTTP_QUERY, String.class); if (queryString != null) { skipRequestHeaders = URISupport.parseQuery(queryString, false, true); } // Need to remove the Host key as it should be not used exchange.getIn().getHeaders().remove("host"); } HttpMethod method = createMethod(exchange); Message in = exchange.getIn(); String httpProtocolVersion = in.getHeader(Exchange.HTTP_PROTOCOL_VERSION, String.class); if (httpProtocolVersion != null) { // set the HTTP protocol version HttpMethodParams params = method.getParams(); params.setVersion(HttpVersion.parse(httpProtocolVersion)); } HeaderFilterStrategy strategy = getEndpoint().getHeaderFilterStrategy(); // propagate headers as HTTP headers for (Map.Entry<String, Object> entry : in.getHeaders().entrySet()) { String key = entry.getKey(); Object headerValue = in.getHeader(key); if (headerValue != null) { // use an iterator as there can be multiple values. (must not use a delimiter, and allow empty values) final Iterator<?> it = ObjectHelper.createIterator(headerValue, null, true); // the value to add as request header final List<String> values = new ArrayList<String>(); // if its a multi value then check each value if we can add it and for multi values they // should be combined into a single value while (it.hasNext()) { String value = exchange.getContext().getTypeConverter().convertTo(String.class, it.next()); // we should not add headers for the parameters in the uri if we bridge the endpoint // as then we would duplicate headers on both the endpoint uri, and in HTTP headers as well if (skipRequestHeaders != null && skipRequestHeaders.containsKey(key)) { continue; } if (value != null && strategy != null && !strategy.applyFilterToCamelHeaders(key, value, exchange)) { values.add(value); } } // add the value(s) as a http request header if (values.size() > 0) { // use the default toString of a ArrayList to create in the form [xxx, yyy] // if multi valued, for a single value, then just output the value as is String s = values.size() > 1 ? values.toString() : values.get(0); method.addRequestHeader(key, s); } } } if (getEndpoint().getCookieHandler() != null) { // disable the per endpoint cookie handling method.getParams().setCookiePolicy(CookiePolicy.IGNORE_COOKIES); Map<String, List<String>> cookieHeaders = getEndpoint().getCookieHandler().loadCookies(exchange, new URI(method.getURI().getEscapedURI())); for (Map.Entry<String, List<String>> entry : cookieHeaders.entrySet()) { String key = entry.getKey(); if (entry.getValue().size() > 0) { // use the default toString of a ArrayList to create in the form [xxx, yyy] // if multi valued, for a single value, then just output the value as is String s = entry.getValue().size() > 1 ? entry.getValue().toString() : entry.getValue().get(0); method.addRequestHeader(key, s); } } } if (getEndpoint().isConnectionClose()) { method.addRequestHeader("Connection", "close"); } // lets store the result in the output message. try { if (LOG.isDebugEnabled()) { LOG.debug("Executing http {} method: {}", method.getName(), method.getURI().toString()); } int responseCode = executeMethod(method); LOG.debug("Http responseCode: {}", responseCode); if (!throwException) { // if we do not use failed exception then populate response for all response codes populateResponse(exchange, method, in, strategy, responseCode); } else { boolean ok = HttpHelper.isStatusCodeOk(responseCode, getEndpoint().getOkStatusCodeRange()); if (ok) { // only populate response for OK response populateResponse(exchange, method, in, strategy, responseCode); } else { // operation failed so populate exception to throw throw populateHttpOperationFailedException(exchange, method, responseCode); } } } finally { method.releaseConnection(); } } @Override public HttpEndpoint getEndpoint() { return (HttpEndpoint) super.getEndpoint(); } protected void populateResponse(Exchange exchange, HttpMethod method, Message in, HeaderFilterStrategy strategy, int responseCode) throws IOException, ClassNotFoundException, URISyntaxException { //We just make the out message is not create when extractResponseBody throws exception, Object response = extractResponseBody(method, exchange, getEndpoint().isIgnoreResponseBody()); Message answer = exchange.getOut(); answer.setHeader(Exchange.HTTP_RESPONSE_CODE, responseCode); answer.setHeader(Exchange.HTTP_RESPONSE_TEXT, method.getStatusText()); answer.setBody(response); // propagate HTTP response headers Header[] headers = method.getResponseHeaders(); Map<String, List<String>> m = new HashMap<String, List<String>>(); for (Header header : headers) { String name = header.getName(); String value = header.getValue(); m.put(name, Collections.singletonList(value)); if (name.toLowerCase().equals("content-type")) { name = Exchange.CONTENT_TYPE; exchange.setProperty(Exchange.CHARSET_NAME, IOHelper.getCharsetNameFromContentType(value)); } // use http helper to extract parameter value as it may contain multiple values Object extracted = HttpHelper.extractHttpParameterValue(value); if (strategy != null && !strategy.applyFilterToExternalHeaders(name, extracted, exchange)) { HttpHelper.appendHeader(answer.getHeaders(), name, extracted); } } // handle cookies if (getEndpoint().getCookieHandler() != null) { getEndpoint().getCookieHandler().storeCookies(exchange, new URI(method.getURI().getEscapedURI()), m); } // endpoint might be configured to copy headers from in to out // to avoid overriding existing headers with old values just // filter the http protocol headers if (getEndpoint().isCopyHeaders()) { MessageHelper.copyHeaders(exchange.getIn(), answer, httpProtocolHeaderFilterStrategy, false); } } protected Exception populateHttpOperationFailedException(Exchange exchange, HttpMethod method, int responseCode) throws IOException, ClassNotFoundException, URISyntaxException { Exception answer; String uri = method.getURI().toString(); String statusText = method.getStatusLine() != null ? method.getStatusLine().getReasonPhrase() : null; Map<String, String> headers = extractResponseHeaders(method.getResponseHeaders()); // handle cookies if (getEndpoint().getCookieHandler() != null) { Map<String, List<String>> m = new HashMap<String, List<String>>(); for (Entry<String, String> e : headers.entrySet()) { m.put(e.getKey(), Collections.singletonList(e.getValue())); } getEndpoint().getCookieHandler().storeCookies(exchange, new URI(method.getURI().getEscapedURI()), m); } Object responseBody = extractResponseBody(method, exchange, getEndpoint().isIgnoreResponseBody()); if (transferException && responseBody != null && responseBody instanceof Exception) { // if the response was a serialized exception then use that return (Exception) responseBody; } // make a defensive copy of the response body in the exception so its detached from the cache String copy = null; if (responseBody != null) { copy = exchange.getContext().getTypeConverter().convertTo(String.class, exchange, responseBody); } if (responseCode >= 300 && responseCode < 400) { String redirectLocation; Header locationHeader = method.getResponseHeader("location"); if (locationHeader != null) { redirectLocation = locationHeader.getValue(); answer = new HttpOperationFailedException(uri, responseCode, statusText, redirectLocation, headers, copy); } else { // no redirect location answer = new HttpOperationFailedException(uri, responseCode, statusText, null, headers, copy); } } else { // internal server error (error code 500) answer = new HttpOperationFailedException(uri, responseCode, statusText, null, headers, copy); } return answer; } /** * Strategy when executing the method (calling the remote server). * * @param method the method to execute * @return the response code * @throws IOException can be thrown */ protected int executeMethod(HttpMethod method) throws IOException { return httpClient.executeMethod(method); } /** * Extracts the response headers * * @param responseHeaders the headers * @return the extracted headers or <tt>null</tt> if no headers existed */ protected static Map<String, String> extractResponseHeaders(Header[] responseHeaders) { if (responseHeaders == null || responseHeaders.length == 0) { return null; } Map<String, String> answer = new HashMap<String, String>(); for (Header header : responseHeaders) { answer.put(header.getName(), header.getValue()); } return answer; } /** * Extracts the response from the method as a InputStream. * * @param method the method that was executed * @param ignoreResponseBody if it is true, camel don't read the response and cached the input stream * @return the response either as a stream, or as a deserialized java object * @throws IOException can be thrown */ protected Object extractResponseBody(HttpMethod method, Exchange exchange, boolean ignoreResponseBody) throws IOException, ClassNotFoundException { InputStream is = method.getResponseBodyAsStream(); if (is == null) { return null; } Header header = method.getResponseHeader(Exchange.CONTENT_ENCODING); String contentEncoding = header != null ? header.getValue() : null; if (!exchange.getProperty(Exchange.SKIP_GZIP_ENCODING, Boolean.FALSE, Boolean.class)) { is = GZIPHelper.uncompressGzip(contentEncoding, is); } // Honor the character encoding String contentType = null; header = method.getResponseHeader("content-type"); if (header != null) { contentType = header.getValue(); // find the charset and set it to the Exchange HttpHelper.setCharsetFromContentType(contentType, exchange); } // if content type is a serialized java object then de-serialize it back to a Java object if (contentType != null && contentType.equals(HttpConstants.CONTENT_TYPE_JAVA_SERIALIZED_OBJECT)) { // only deserialize java if allowed if (getEndpoint().getComponent().isAllowJavaSerializedObject() || getEndpoint().isTransferException()) { return HttpHelper.deserializeJavaObjectFromStream(is, exchange.getContext()); } else { // empty response return null; } } else { if (!getEndpoint().isDisableStreamCache()) { // wrap the response in a stream cache so its re-readable InputStream response = null; if (!ignoreResponseBody) { response = doExtractResponseBodyAsStream(is, exchange); } return response; } else { // use the response stream as-is return is; } } } private static InputStream doExtractResponseBodyAsStream(InputStream is, Exchange exchange) throws IOException { // As httpclient is using a AutoCloseInputStream, it will be closed when the connection is closed // we need to cache the stream for it. CachedOutputStream cos = null; try { // This CachedOutputStream will not be closed when the exchange is onCompletion cos = new CachedOutputStream(exchange, false); IOHelper.copy(is, cos); // When the InputStream is closed, the CachedOutputStream will be closed return cos.getWrappedInputStream(); } catch (IOException ex) { // try to close the CachedOutputStream when we get the IOException try { cos.close(); } catch (IOException ignore) { //do nothing here } throw ex; } finally { IOHelper.close(is, "Extracting response body", LOG); } } /** * Creates the HttpMethod to use to call the remote server, either its GET or POST. * * @param exchange the exchange * @return the created method as either GET or POST * @throws CamelExchangeException is thrown if error creating RequestEntity */ @SuppressWarnings("deprecation") protected HttpMethod createMethod(Exchange exchange) throws Exception { // creating the url to use takes 2-steps String url = HttpHelper.createURL(exchange, getEndpoint()); URI uri = HttpHelper.createURI(exchange, url, getEndpoint()); // get the url and query string from the uri url = uri.toASCIIString(); String queryString = uri.getRawQuery(); // execute any custom url rewrite String rewriteUrl = HttpHelper.urlRewrite(exchange, url, getEndpoint(), this); if (rewriteUrl != null) { // update url and query string from the rewritten url url = rewriteUrl; uri = new URI(url); // use raw query to have uri decimal encoded which http client requires queryString = uri.getRawQuery(); } // remove query string as http client does not accept that if (url.indexOf('?') != -1) { url = url.substring(0, url.indexOf('?')); } // create http holder objects for the request RequestEntity requestEntity = createRequestEntity(exchange); String methodName = HttpHelper.createMethod(exchange, getEndpoint(), requestEntity != null).name(); HttpMethods methodsToUse = HttpMethods.valueOf(methodName); HttpMethod method = methodsToUse.createMethod(url); if (queryString != null) { // need to encode query string queryString = UnsafeUriCharactersEncoder.encode(queryString); method.setQueryString(queryString); } LOG.trace("Using URL: {} with method: {}", url, method); if (methodsToUse.isEntityEnclosing()) { ((EntityEnclosingMethod) method).setRequestEntity(requestEntity); if (requestEntity != null && requestEntity.getContentType() == null) { LOG.debug("No Content-Type provided for URL: {} with exchange: {}", url, exchange); } } // there must be a host on the method if (method.getHostConfiguration().getHost() == null) { throw new IllegalArgumentException("Invalid uri: " + url + ". If you are forwarding/bridging http endpoints, then enable the bridgeEndpoint option on the endpoint: " + getEndpoint()); } return method; } /** * Creates a holder object for the data to send to the remote server. * * @param exchange the exchange with the IN message with data to send * @return the data holder * @throws CamelExchangeException is thrown if error creating RequestEntity */ protected RequestEntity createRequestEntity(Exchange exchange) throws CamelExchangeException { Message in = exchange.getIn(); if (in.getBody() == null) { return null; } RequestEntity answer = in.getBody(RequestEntity.class); if (answer == null) { try { Object data = in.getBody(); if (data != null) { String contentType = ExchangeHelper.getContentType(exchange); if (contentType != null && HttpConstants.CONTENT_TYPE_JAVA_SERIALIZED_OBJECT.equals(contentType)) { if (!getEndpoint().getComponent().isAllowJavaSerializedObject()) { throw new CamelExchangeException("Content-type " + HttpConstants.CONTENT_TYPE_JAVA_SERIALIZED_OBJECT + " is not allowed", exchange); } // serialized java object Serializable obj = in.getMandatoryBody(Serializable.class); // write object to output stream ByteArrayOutputStream bos = new ByteArrayOutputStream(); HttpHelper.writeObjectToStream(bos, obj); answer = new ByteArrayRequestEntity(bos.toByteArray(), HttpConstants.CONTENT_TYPE_JAVA_SERIALIZED_OBJECT); IOHelper.close(bos); } else if (data instanceof File || data instanceof GenericFile) { // file based (could potentially also be a FTP file etc) File file = in.getBody(File.class); if (file != null) { answer = new FileRequestEntity(file, contentType); } } else if (data instanceof String) { // be a bit careful with String as any type can most likely be converted to String // so we only do an instanceof check and accept String if the body is really a String // do not fallback to use the default charset as it can influence the request // (for example application/x-www-form-urlencoded forms being sent) String charset = IOHelper.getCharsetName(exchange, false); answer = new StringRequestEntity((String) data, contentType, charset); } // fallback as input stream if (answer == null) { // force the body as an input stream since this is the fallback InputStream is = in.getMandatoryBody(InputStream.class); answer = new InputStreamRequestEntity(is, contentType); } } } catch (UnsupportedEncodingException e) { throw new CamelExchangeException("Error creating RequestEntity from message body", exchange, e); } catch (IOException e) { throw new CamelExchangeException("Error serializing message body", exchange, e); } } return answer; } public HttpClient getHttpClient() { return httpClient; } public void setHttpClient(HttpClient httpClient) { this.httpClient = httpClient; } }