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