| /** |
| * 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.util.ArrayList; |
| import java.util.HashMap; |
| import java.util.Iterator; |
| import java.util.List; |
| import java.util.Map; |
| |
| 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.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; |
| |
| /** |
| * @version |
| */ |
| 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); |
| } |
| } |
| } |
| |
| // 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 { |
| //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(); |
| for (Header header : headers) { |
| String name = header.getName(); |
| String value = header.getValue(); |
| 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); |
| } |
| } |
| |
| // 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 { |
| Exception answer; |
| |
| String uri = method.getURI().toString(); |
| String statusText = method.getStatusLine() != null ? method.getStatusLine().getReasonPhrase() : null; |
| Map<String, String> headers = extractResponseHeaders(method.getResponseHeaders()); |
| |
| 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; |
| } |
| } |