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.slf4j.Logger;
054    import org.slf4j.LoggerFactory;
055    
056    /**
057     * @version 
058     */
059    public class HttpProducer extends DefaultProducer {
060        private static final transient Logger LOG = LoggerFactory.getLogger(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            //We just make the out message is not create when extractResponseBody throws exception,
129            Object response = extractResponseBody(method, exchange);
130            Message answer = exchange.getOut();
131    
132            answer.setHeaders(in.getHeaders());
133            answer.setHeader(Exchange.HTTP_RESPONSE_CODE, responseCode);
134            answer.setBody(response);
135    
136            // propagate HTTP response headers
137            Header[] headers = method.getResponseHeaders();
138            for (Header header : headers) {
139                String name = header.getName();
140                String value = header.getValue();
141                if (name.toLowerCase().equals("content-type")) {
142                    name = Exchange.CONTENT_TYPE;
143                }
144                if (strategy != null && !strategy.applyFilterToExternalHeaders(name, value, exchange)) {
145                    answer.setHeader(name, value);
146                }
147            }
148        }
149    
150        protected Exception populateHttpOperationFailedException(Exchange exchange, HttpMethod method, int responseCode) throws IOException, ClassNotFoundException {
151            Exception answer;
152    
153            String uri = method.getURI().toString();
154            String statusText = method.getStatusLine() != null ? method.getStatusLine().getReasonPhrase() : null;
155            Map<String, String> headers = extractResponseHeaders(method.getResponseHeaders());
156    
157            Object responseBody = extractResponseBody(method, exchange);
158            if (transferException && responseBody != null && responseBody instanceof Exception) {
159                // if the response was a serialized exception then use that
160                return (Exception) responseBody;
161            }
162    
163            // make a defensive copy of the response body in the exception so its detached from the cache
164            String copy = null;
165            if (responseBody != null) {
166                copy = exchange.getContext().getTypeConverter().convertTo(String.class, exchange, responseBody);
167            }
168    
169            if (responseCode >= 300 && responseCode < 400) {
170                String redirectLocation;
171                Header locationHeader = method.getResponseHeader("location");
172                if (locationHeader != null) {
173                    redirectLocation = locationHeader.getValue();
174                    answer = new HttpOperationFailedException(uri, responseCode, statusText, redirectLocation, headers, copy);
175                } else {
176                    // no redirect location
177                    answer = new HttpOperationFailedException(uri, responseCode, statusText, null, headers, copy);
178                }
179            } else {
180                // internal server error (error code 500)
181                answer = new HttpOperationFailedException(uri, responseCode, statusText, null, headers, copy);
182            }
183    
184            return answer;
185        }
186    
187        /**
188         * Strategy when executing the method (calling the remote server).
189         *
190         * @param method the method to execute
191         * @return the response code
192         * @throws IOException can be thrown
193         */
194        protected int executeMethod(HttpMethod method) throws IOException {
195            return httpClient.executeMethod(method);
196        }
197    
198        /**
199         * Extracts the response headers
200         *
201         * @param responseHeaders the headers
202         * @return the extracted headers or <tt>null</tt> if no headers existed
203         */
204        protected static Map<String, String> extractResponseHeaders(Header[] responseHeaders) {
205            if (responseHeaders == null || responseHeaders.length == 0) {
206                return null;
207            }
208    
209            Map<String, String> answer = new HashMap<String, String>();
210            for (Header header : responseHeaders) {
211                answer.put(header.getName(), header.getValue());
212            }
213    
214            return answer;
215        }
216    
217        /**
218         * Extracts the response from the method as a InputStream.
219         *
220         * @param method the method that was executed
221         * @return the response either as a stream, or as a deserialized java object
222         * @throws IOException can be thrown
223         */
224        protected static Object extractResponseBody(HttpMethod method, Exchange exchange) throws IOException, ClassNotFoundException {
225            InputStream is = method.getResponseBodyAsStream();
226            if (is == null) {
227                return null;
228            }
229    
230            Header header = method.getResponseHeader(Exchange.CONTENT_ENCODING);
231            String contentEncoding = header != null ? header.getValue() : null;
232    
233            if (!exchange.getProperty(Exchange.SKIP_GZIP_ENCODING, Boolean.FALSE, Boolean.class)) {
234                is = GZIPHelper.uncompressGzip(contentEncoding, is);
235            }
236    
237            // Honor the character encoding
238            String contentType = null;
239            header = method.getResponseHeader("content-type");
240            if (header != null) {
241                contentType = header.getValue();
242                // find the charset and set it to the Exchange
243                HttpHelper.setCharsetFromContentType(contentType, exchange);
244            }
245            InputStream response = doExtractResponseBodyAsStream(is, exchange);
246            // if content type is a serialized java object then de-serialize it back to a Java object
247            if (contentType != null && contentType.equals(HttpConstants.CONTENT_TYPE_JAVA_SERIALIZED_OBJECT)) {
248                return HttpHelper.deserializeJavaObjectFromStream(response);
249            } else {
250                return response;
251            }
252        }
253    
254        private static InputStream doExtractResponseBodyAsStream(InputStream is, Exchange exchange) throws IOException {
255            // As httpclient is using a AutoCloseInputStream, it will be closed when the connection is closed
256            // we need to cache the stream for it.
257            try {
258                // This CachedOutputStream will not be closed when the exchange is onCompletion
259                CachedOutputStream cos = new CachedOutputStream(exchange, false);
260                IOHelper.copy(is, cos);
261                // When the InputStream is closed, the CachedOutputStream will be closed
262                return cos.getWrappedInputStream();
263            } finally {
264                IOHelper.close(is, "Extracting response body", LOG);
265            }
266        }
267    
268        /**
269         * Creates the HttpMethod to use to call the remote server, either its GET or POST.
270         *
271         * @param exchange the exchange
272         * @return the created method as either GET or POST
273         * @throws CamelExchangeException is thrown if error creating RequestEntity
274         */
275        protected HttpMethod createMethod(Exchange exchange) throws CamelExchangeException {
276    
277            String url = HttpHelper.createURL(exchange, getEndpoint());
278    
279            RequestEntity requestEntity = createRequestEntity(exchange);
280            HttpMethods methodToUse = HttpHelper.createMethod(exchange, getEndpoint(), requestEntity != null);
281            HttpMethod method = methodToUse.createMethod(url);
282    
283            // is a query string provided in the endpoint URI or in a header (header overrules endpoint)
284            String queryString = exchange.getIn().getHeader(Exchange.HTTP_QUERY, String.class);
285            if (queryString == null) {
286                queryString = getEndpoint().getHttpUri().getRawQuery();
287            }
288            if (queryString != null) {
289                // need to make sure the queryString is URI safe
290                method.setQueryString(queryString);
291            }
292    
293            if (methodToUse.isEntityEnclosing()) {
294                ((EntityEnclosingMethod) method).setRequestEntity(requestEntity);
295                if (requestEntity != null && requestEntity.getContentType() == null) {
296                    if (LOG.isDebugEnabled()) {
297                        LOG.debug("No Content-Type provided for URL: " + url + " with exchange: " + exchange);
298                    }
299                }
300            }
301    
302            return method;
303        }
304    
305        /**
306         * Creates a holder object for the data to send to the remote server.
307         *
308         * @param exchange the exchange with the IN message with data to send
309         * @return the data holder
310         * @throws CamelExchangeException is thrown if error creating RequestEntity
311         */
312        protected RequestEntity createRequestEntity(Exchange exchange) throws CamelExchangeException {
313            Message in = exchange.getIn();
314            if (in.getBody() == null) {
315                return null;
316            }
317    
318            RequestEntity answer = in.getBody(RequestEntity.class);
319            if (answer == null) {
320                try {
321                    Object data = in.getBody();
322                    if (data != null) {
323                        String contentType = ExchangeHelper.getContentType(exchange);
324    
325                        if (contentType != null && HttpConstants.CONTENT_TYPE_JAVA_SERIALIZED_OBJECT.equals(contentType)) {
326                            // serialized java object
327                            Serializable obj = in.getMandatoryBody(Serializable.class);
328                            // write object to output stream
329                            ByteArrayOutputStream bos = new ByteArrayOutputStream();
330                            HttpHelper.writeObjectToStream(bos, obj);
331                            answer = new ByteArrayRequestEntity(bos.toByteArray(), HttpConstants.CONTENT_TYPE_JAVA_SERIALIZED_OBJECT);
332                            IOHelper.close(bos);
333                        } else if (data instanceof File || data instanceof GenericFile) {
334                            // file based (could potentially also be a FTP file etc)
335                            File file = in.getBody(File.class);
336                            if (file != null) {
337                                answer = new FileRequestEntity(file, contentType);
338                            }
339                        } else if (data instanceof String) {
340                            // be a bit careful with String as any type can most likely be converted to String
341                            // so we only do an instanceof check and accept String if the body is really a String
342                            // do not fallback to use the default charset as it can influence the request
343                            // (for example application/x-www-form-urlencoded forms being sent)
344                            String charset = IOConverter.getCharsetName(exchange, false);
345                            answer = new StringRequestEntity((String) data, contentType, charset);
346                        }
347                        // fallback as input stream
348                        if (answer == null) {
349                            // force the body as an input stream since this is the fallback
350                            InputStream is = in.getMandatoryBody(InputStream.class);
351                            answer = new InputStreamRequestEntity(is, contentType);
352                        }
353                    }
354                } catch (UnsupportedEncodingException e) {
355                    throw new CamelExchangeException("Error creating RequestEntity from message body", exchange, e);
356                } catch (IOException e) {
357                    throw new CamelExchangeException("Error serializing message body", exchange, e);
358                }
359            }
360            return answer;
361        }
362    
363        public HttpClient getHttpClient() {
364            return httpClient;
365        }
366    
367        public void setHttpClient(HttpClient httpClient) {
368            this.httpClient = httpClient;
369        }
370    }