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 }