/**
* 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.jetty9;
import java.io.ByteArrayOutputStream;
import java.io.Closeable;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.Method;
import java.net.MalformedURLException;
import java.nio.ByteBuffer;
import java.util.Collection;
import java.util.Map;
import java.util.TreeMap;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import static javax.servlet.http.HttpServletResponse.SC_OK;
import org.apache.camel.AsyncCallback;
import org.apache.camel.CamelExchangeException;
import org.apache.camel.Exchange;
import org.apache.camel.ExchangeTimedOutException;
import org.apache.camel.StreamCache;
import org.apache.camel.component.jetty.JettyContentExchange;
import org.apache.camel.component.jetty.JettyHttpBinding;
import org.apache.camel.converter.stream.OutputStreamBuilder;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.api.Request;
import org.eclipse.jetty.client.api.Response;
import org.eclipse.jetty.client.api.Result;
import org.eclipse.jetty.client.util.BytesContentProvider;
import org.eclipse.jetty.client.util.InputStreamContentProvider;
import org.eclipse.jetty.client.util.InputStreamResponseListener;
import org.eclipse.jetty.client.util.StringContentProvider;
import org.eclipse.jetty.http.HttpFields;
import org.eclipse.jetty.util.Callback;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Jetty specific exchange which keeps track of the the request and response.
*/
public class JettyContentExchange9 implements JettyContentExchange {
private static final Logger LOG = LoggerFactory.getLogger(JettyContentExchange9.class);
private volatile Exchange exchange;
private volatile AsyncCallback callback;
private volatile JettyHttpBinding jettyBinding;
private volatile HttpClient client;
private final CountDownLatch done = new CountDownLatch(1);
private Request request;
private Response response;
private byte[] responseContent;
private String requestContentType;
private boolean supportRedirect;
public void init(Exchange exchange, JettyHttpBinding jettyBinding,
final HttpClient client, AsyncCallback callback) {
this.exchange = exchange;
this.jettyBinding = jettyBinding;
this.client = client;
this.callback = callback;
}
protected void onRequestComplete() {
LOG.trace("onRequestComplete");
closeRequestContentSource();
}
protected void onResponseComplete(Result result, byte[] content) {
LOG.trace("onResponseComplete");
done.countDown();
this.response = result.getResponse();
this.responseContent = content;
if (callback == null) {
// this is only for the async callback
return;
}
try {
jettyBinding.populateResponse(exchange, this);
} catch (Exception e) {
exchange.setException(e);
} finally {
callback.done(false);
}
}
protected void onExpire() {
LOG.trace("onExpire");
// need to close the request input stream
closeRequestContentSource();
doTaskCompleted(new ExchangeTimedOutException(exchange, client.getConnectTimeout()));
}
protected void onException(Throwable ex) {
LOG.trace("onException {}", ex);
// need to close the request input stream
closeRequestContentSource();
doTaskCompleted(ex);
}
protected void onConnectionFailed(Throwable ex) {
LOG.trace("onConnectionFailed {}", ex);
// need to close the request input stream
closeRequestContentSource();
doTaskCompleted(ex);
}
public byte[] getBody() {
// must return the content as raw bytes
return getResponseContentBytes();
}
public String getUrl() {
try {
return this.request.getURI().toURL().toExternalForm();
} catch (MalformedURLException e) {
throw new IllegalStateException(e.getMessage(), e);
}
}
protected void closeRequestContentSource() {
tryClose(this.request.getContent());
}
private void tryClose(Object obj) {
if (obj instanceof Closeable) {
try {
((Closeable) obj).close();
} catch (IOException e) {
// Ignore
}
}
}
protected void doTaskCompleted(Throwable ex) {
if (ex instanceof TimeoutException) {
exchange.setException(new ExchangeTimedOutException(exchange, request.getTimeout()));
} else {
exchange.setException(new CamelExchangeException("JettyClient failed cause by: " + ex.getMessage(), exchange, ex));
}
done.countDown();
if (callback != null) {
// now invoke callback to indicate we are done async
callback.done(false);
}
}
public void setRequestContentType(String contentType) {
this.requestContentType = contentType;
}
public int getResponseStatus() {
return this.response.getStatus();
}
public void setMethod(String method) {
this.request.method(method);
}
public void setTimeout(long timeout) {
this.request.timeout(timeout, TimeUnit.MILLISECONDS);
}
public void setURL(String url) {
this.request = client.newRequest(url);
}
public void setRequestContent(byte[] byteArray) {
this.request.content(new BytesContentProvider(byteArray), this.requestContentType);
}
public void setRequestContent(String data, String charset) throws UnsupportedEncodingException {
StringContentProvider cp = charset != null ? new StringContentProvider(data, charset) : new StringContentProvider(data);
this.request.content(cp, this.requestContentType);
}
public void setRequestContent(InputStream ins) {
this.request.content(new InputStreamContentProvider(ins), this.requestContentType);
}
public void setRequestContent(InputStream ins, int contentLength) {
this.request.content(new CamelInputStreamContentProvider(ins, contentLength), this.requestContentType);
}
public void addRequestHeader(String key, String s) {
this.request.header(key, s);
}
public void send(HttpClient client) throws IOException {
org.eclipse.jetty.client.api.Request.Listener listener = new Request.Listener.Adapter() {
@Override
public void onSuccess(Request request) {
onRequestComplete();
}
@Override
public void onFailure(Request request, Throwable failure) {
onConnectionFailed(failure);
}
};
InputStreamResponseListener responseListener = new InputStreamResponseListener() {
OutputStreamBuilder osb = OutputStreamBuilder.withExchange(exchange);
@Override
public void onContent(Response response, ByteBuffer content, Callback callback) {
byte[] buffer = new byte[content.limit()];
content.get(buffer);
try {
osb.write(buffer);
callback.succeeded();
} catch (IOException e) {
callback.failed(e);
}
}
@Override
public void onComplete(Result result) {
if (result.isFailed()) {
doTaskCompleted(result.getFailure());
} else {
try {
Object content = osb.build();
if (content instanceof byte[]) {
onResponseComplete(result, (byte[]) content);
} else {
StreamCache cos = (StreamCache) content;
ByteArrayOutputStream baos = new ByteArrayOutputStream();
cos.writeTo(baos);
onResponseComplete(result, baos.toByteArray());
}
} catch (IOException e) {
doTaskCompleted(e);
}
}
}
};
request.followRedirects(supportRedirect).listener(listener).send(responseListener);
}
protected void setResponse(Response response) {
this.response = response;
}
public byte[] getResponseContentBytes() {
return responseContent;
}
private Map<String, Collection<String>> getFieldsAsMap(HttpFields fields) {
final Map<String, Collection<String>> result = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
for (String name : getFieldNamesCollection(fields)) {
result.put(name, fields.getValuesList(name));
}
return result;
}
@SuppressWarnings("unchecked")
private Collection<String> getFieldNamesCollection(HttpFields fields) {
try {
return fields.getFieldNamesCollection();
} catch (NoSuchMethodError e) {
try {
// In newer versions of Jetty the return type has been changed to Set.
// This causes problems at byte-code level. Try recovering.
Method reflGetFieldNamesCollection = HttpFields.class.getMethod("getFieldNamesCollection");
Object result = reflGetFieldNamesCollection.invoke(fields);
return (Collection<String>) result;
} catch (Exception reflectionException) {
// Suppress, throwing the original exception
throw e;
}
}
}
public Map<String, Collection<String>> getRequestHeaders() {
return getFieldsAsMap(request.getHeaders());
}
public Map<String, Collection<String>> getResponseHeaders() {
return getFieldsAsMap(response.getHeaders());
}
@Override
public void setSupportRedirect(boolean supportRedirect) {
this.supportRedirect = supportRedirect;
}
}