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