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    }