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