/**
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.camel.component.ahc;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.Serializable;
import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.nio.charset.Charset;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import org.apache.camel.CamelExchangeException;
import org.apache.camel.Exchange;
import org.apache.camel.Message;
import org.apache.camel.component.ahc.helper.AhcHelper;
import org.apache.camel.component.file.GenericFile;
import org.apache.camel.spi.HeaderFilterStrategy;
import org.apache.camel.util.ExchangeHelper;
import org.apache.camel.util.GZIPHelper;
import org.apache.camel.util.IOHelper;
import org.apache.camel.util.MessageHelper;
import org.apache.camel.util.ObjectHelper;
import org.asynchttpclient.HttpResponseHeaders;
import org.asynchttpclient.HttpResponseStatus;
import org.asynchttpclient.Request;
import org.asynchttpclient.RequestBuilder;
import org.asynchttpclient.request.body.generator.BodyGenerator;
import org.asynchttpclient.request.body.generator.ByteArrayBodyGenerator;
import org.asynchttpclient.request.body.generator.FileBodyGenerator;
import org.asynchttpclient.request.body.generator.InputStreamBodyGenerator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class DefaultAhcBinding implements AhcBinding {
protected final Logger log = LoggerFactory.getLogger(this.getClass());
protected HeaderFilterStrategy httpProtocolHeaderFilterStrategy = new HttpProtocolHeaderFilterStrategy();
public Request prepareRequest(AhcEndpoint endpoint, Exchange exchange) throws CamelExchangeException {
if (endpoint.isBridgeEndpoint()) {
exchange.setProperty(Exchange.SKIP_GZIP_ENCODING, Boolean.TRUE);
// Need to remove the Host key as it should be not used
exchange.getIn().getHeaders().remove("host");
}
RequestBuilder builder = new RequestBuilder();
URI uri;
try {
// creating the url to use takes 2-steps
String url = AhcHelper.createURL(exchange, endpoint);
uri = AhcHelper.createURI(exchange, url, endpoint);
// get the url from the uri
url = uri.toASCIIString();
log.trace("Setting url {}", url);
builder.setUrl(url);
} catch (Exception e) {
throw new CamelExchangeException("Error creating URL", exchange, e);
}
String method = extractMethod(exchange);
log.trace("Setting method {}", method);
builder.setMethod(method);
populateHeaders(builder, endpoint, exchange);
populateCookieHeaders(builder, endpoint, exchange, uri);
populateBody(builder, endpoint, exchange);
return builder.build();
}
protected String extractMethod(Exchange exchange) {
// prefer method from header
String method = exchange.getIn().getHeader(Exchange.HTTP_METHOD, String.class);
if (method != null) {
return method;
}
// if there is a body then do a POST otherwise a GET
boolean hasBody = exchange.getIn().getBody() != null;
return hasBody ? "POST" : "GET";
}
protected void populateHeaders(RequestBuilder builder, AhcEndpoint endpoint, Exchange exchange) {
HeaderFilterStrategy strategy = endpoint.getHeaderFilterStrategy();
// propagate headers as HTTP headers
for (Map.Entry<String, Object> entry : exchange.getIn().getHeaders().entrySet()) {
String headerValue = exchange.getIn().getHeader(entry.getKey(), String.class);
if (strategy != null && !strategy.applyFilterToCamelHeaders(entry.getKey(), headerValue, exchange)) {
if (log.isTraceEnabled()) {
log.trace("Adding header {} = {}", entry.getKey(), headerValue);
}
builder.addHeader(entry.getKey(), headerValue);
}
}
if (endpoint.isConnectionClose()) {
builder.addHeader("Connection", "close");
}
}
private void populateCookieHeaders(RequestBuilder builder, AhcEndpoint endpoint, Exchange exchange, URI uri) throws CamelExchangeException {
if (endpoint.getCookieHandler() != null) {
try {
Map<String, List<String>> cookieHeaders = endpoint.getCookieHandler().loadCookies(exchange, uri);
for (Map.Entry<String, List<String>> entry : cookieHeaders.entrySet()) {
String key = entry.getKey();
if (entry.getValue().size() > 0) {
// use the default toString of a ArrayList to create in the form [xxx, yyy]
// if multi valued, for a single value, then just output the value as is
String s = entry.getValue().size() > 1 ? entry.getValue().toString() : entry.getValue().get(0);
builder.addHeader(key, s);
}
}
} catch (IOException e) {
throw new CamelExchangeException("Error loading cookies", exchange, e);
}
}
}
protected void populateBody(RequestBuilder builder, AhcEndpoint endpoint, Exchange exchange) throws CamelExchangeException {
Message in = exchange.getIn();
if (in.getBody() == null) {
return;
}
String contentType = ExchangeHelper.getContentType(exchange);
BodyGenerator body = in.getBody(BodyGenerator.class);
String charset = IOHelper.getCharsetName(exchange, false);
if (body == null) {
try {
Object data = in.getBody();
if (data != null) {
if (contentType != null && AhcConstants.CONTENT_TYPE_JAVA_SERIALIZED_OBJECT.equals(contentType)) {
if (!endpoint.getComponent().isAllowJavaSerializedObject()) {
throw new CamelExchangeException("Content-type " + AhcConstants.CONTENT_TYPE_JAVA_SERIALIZED_OBJECT + " is not allowed", exchange);
}
// serialized java object
Serializable obj = in.getMandatoryBody(Serializable.class);
// write object to output stream
ByteArrayOutputStream bos = new ByteArrayOutputStream(endpoint.getBufferSize());
AhcHelper.writeObjectToStream(bos, obj);
byte[] bytes = bos.toByteArray();
body = new ByteArrayBodyGenerator(bytes);
IOHelper.close(bos);
} else if (data instanceof File || data instanceof GenericFile) {
// file based (could potentially also be a FTP file etc)
File file = in.getBody(File.class);
if (file != null) {
body = new FileBodyGenerator(file);
}
} else if (data instanceof String) {
// be a bit careful with String as any type can most likely be converted to String
// so we only do an instanceof check and accept String if the body is really a String
// do not fallback to use the default charset as it can influence the request
// (for example application/x-www-form-urlencoded forms being sent)
if (charset != null) {
body = new ByteArrayBodyGenerator(((String) data).getBytes(charset));
} else {
body = new ByteArrayBodyGenerator(((String) data).getBytes());
}
}
// fallback as input stream
if (body == null) {
// force the body as an input stream since this is the fallback
InputStream is = in.getMandatoryBody(InputStream.class);
body = new InputStreamBodyGenerator(is);
}
}
} catch (UnsupportedEncodingException e) {
throw new CamelExchangeException("Error creating BodyGenerator from message body", exchange, e);
} catch (IOException e) {
throw new CamelExchangeException("Error serializing message body", exchange, e);
}
}
if (body != null) {
log.trace("Setting body {}", body);
builder.setBody(body);
}
if (charset != null) {
log.trace("Setting body charset {}", charset);
builder.setCharset(Charset.forName(charset));
}
// must set content type, even if its null, otherwise it may default to
// application/x-www-form-urlencoded which may not be your intention
log.trace("Setting Content-Type {}", contentType);
if (ObjectHelper.isNotEmpty(contentType)) {
builder.setHeader(Exchange.CONTENT_TYPE, contentType);
}
}
@Override
public void onThrowable(AhcEndpoint endpoint, Exchange exchange, Throwable t) throws Exception {
exchange.setException(t);
}
@Override
public void onStatusReceived(AhcEndpoint endpoint, Exchange exchange, HttpResponseStatus responseStatus) throws Exception {
// preserve headers from in by copying any non existing headers
// to avoid overriding existing headers with old values
// Just filter the http protocol headers
MessageHelper.copyHeaders(exchange.getIn(), exchange.getOut(), httpProtocolHeaderFilterStrategy, false);
exchange.getOut().setHeader(Exchange.HTTP_RESPONSE_CODE, responseStatus.getStatusCode());
exchange.getOut().setHeader(Exchange.HTTP_RESPONSE_TEXT, responseStatus.getStatusText());
}
@Override
public void onHeadersReceived(AhcEndpoint endpoint, Exchange exchange, HttpResponseHeaders headers) throws Exception {
List<Entry<String, String>> l = headers.getHeaders().entries();
Map<String, List<String>> m = new HashMap<String, List<String>>();
for (Entry<String, String> entry : headers.getHeaders().entries()) {
String key = entry.getKey();
String value = entry.getValue();
m.put(key, Collections.singletonList(value));
exchange.getOut().getHeaders().put(key, value);
}
// handle cookies
if (endpoint.getCookieHandler() != null) {
try {
// creating the url to use takes 2-steps
String url = AhcHelper.createURL(exchange, endpoint);
URI uri = AhcHelper.createURI(exchange, url, endpoint);
endpoint.getCookieHandler().storeCookies(exchange, uri, m);
} catch (Exception e) {
throw new CamelExchangeException("Error storing cookies", exchange, e);
}
}
}
@Override
public void onComplete(AhcEndpoint endpoint, Exchange exchange, String url, ByteArrayOutputStream os, int contentLength,
int statusCode, String statusText) throws Exception {
// copy from output stream to input stream
os.flush();
os.close();
InputStream is = new ByteArrayInputStream(os.toByteArray());
String contentEncoding = exchange.getOut().getHeader(Exchange.CONTENT_ENCODING, String.class);
if (!exchange.getProperty(Exchange.SKIP_GZIP_ENCODING, Boolean.FALSE, Boolean.class)) {
is = GZIPHelper.uncompressGzip(contentEncoding, is);
}
// Honor the character encoding
String contentType = exchange.getOut().getHeader(Exchange.CONTENT_TYPE, String.class);
if (contentType != null) {
// find the charset and set it to the Exchange
AhcHelper.setCharsetFromContentType(contentType, exchange);
}
Object body = is;
// if content type is a serialized java object then de-serialize it back to a Java object but only if its allowed
// an exception can also be transffered as java object
if (contentType != null && contentType.equals(AhcConstants.CONTENT_TYPE_JAVA_SERIALIZED_OBJECT)) {
if (endpoint.getComponent().isAllowJavaSerializedObject() || endpoint.isTransferException()) {
body = AhcHelper.deserializeJavaObjectFromStream(is);
}
}
if (!endpoint.isThrowExceptionOnFailure()) {
// if we do not use failed exception then populate response for all response codes
populateResponse(exchange, body, contentLength, statusCode);
} else {
if (statusCode >= 100 && statusCode < 300) {
// only populate response for OK response
populateResponse(exchange, body, contentLength, statusCode);
} else {
// operation failed so populate exception to throw
throw populateHttpOperationFailedException(endpoint, exchange, url, body, contentLength, statusCode, statusText);
}
}
}
private Exception populateHttpOperationFailedException(AhcEndpoint endpoint, Exchange exchange, String url,
Object body, int contentLength,
int statusCode, String statusText) {
Exception answer;
if (endpoint.isTransferException() && body != null && body instanceof Exception) {
// if the response was a serialized exception then use that
return (Exception) body;
}
// make a defensive copy of the response body in the exception so its detached from the cache
String copy = null;
if (body != null) {
copy = exchange.getContext().getTypeConverter().convertTo(String.class, exchange, body);
}
Map<String, String> headers = extractResponseHeaders(exchange);
if (statusCode >= 300 && statusCode < 400) {
String redirectLocation = exchange.getOut().getHeader("Location", String.class);
if (redirectLocation != null) {
answer = new AhcOperationFailedException(url, statusCode, statusText, redirectLocation, headers, copy);
} else {
// no redirect location
answer = new AhcOperationFailedException(url, statusCode, statusText, null, headers, copy);
}
} else {
// internal server error (error code 500)
answer = new AhcOperationFailedException(url, statusCode, statusText, null, headers, copy);
}
return answer;
}
private Map<String, String> extractResponseHeaders(Exchange exchange) {
Map<String, String> answer = new LinkedHashMap<String, String>();
for (Map.Entry<String, Object> entry : exchange.getOut().getHeaders().entrySet()) {
String key = entry.getKey();
String value = exchange.getContext().getTypeConverter().convertTo(String.class, entry.getValue());
if (value != null) {
answer.put(key, value);
}
}
return answer;
}
private void populateResponse(Exchange exchange, Object body, int contentLength, int responseCode) {
exchange.getOut().setBody(body);
exchange.getOut().setHeader(Exchange.CONTENT_LENGTH, contentLength);
}
}