/**
* 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.undertow;
import java.io.Closeable;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.ByteBuffer;
import java.nio.channels.Channel;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.BlockingDeque;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import io.undertow.client.ClientCallback;
import io.undertow.client.ClientConnection;
import io.undertow.client.ClientExchange;
import io.undertow.client.ClientRequest;
import io.undertow.util.HeaderMap;
import io.undertow.util.HttpString;
import org.apache.camel.AsyncCallback;
import org.apache.camel.Exchange;
import org.apache.camel.Message;
import org.apache.camel.http.common.HttpHelper;
import org.apache.camel.http.common.HttpOperationFailedException;
import org.apache.camel.util.ExchangeHelper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xnio.ChannelExceptionHandler;
import org.xnio.ChannelListener;
import org.xnio.ChannelListeners;
import org.xnio.IoUtils;
import org.xnio.channels.StreamSinkChannel;
/**
* Undertow {@link ClientCallback} that will get notified when the HTTP
* connection is ready or when the client failed to connect. It will also handle
* writing the request and reading the response in
* {@link #writeRequest(ClientExchange, ByteBuffer)} and
* {@link #setupResponseListener(ClientExchange)}. The main entry point is
* {@link #completed(ClientConnection)} or {@link #failed(IOException)} in case
* of errors, every error condition that should terminate Camel {@link Exchange}
* should go to {@link #hasFailedWith(Exception)} and successful execution of
* the exchange should end with {@link #finish(Message)}. Any
* {@link ClientCallback}s that are added here should extend
* {@link ErrorHandlingClientCallback}, best way to do that is to use the
* {@link #on(Consumer)} helper method.
*/
class UndertowClientCallback implements ClientCallback<ClientConnection> {
/**
* {@link ClientCallback} that handles failures automatically by propagating
* the exception to Camel {@link Exchange} and notifies Camel that the
* exchange finished by calling {@link AsyncCallback#done(boolean)}.
*/
final class ErrorHandlingClientCallback<T> implements ClientCallback<T> {
private final Consumer<T> consumer;
private ErrorHandlingClientCallback(final Consumer<T> consumer) {
this.consumer = consumer;
}
@Override
public void completed(final T result) {
consumer.accept(result);
}
@Override
public void failed(final IOException e) {
hasFailedWith(e);
}
}
private static final Logger LOG = LoggerFactory.getLogger(UndertowClientCallback.class);
private final ByteBuffer body;
private final AsyncCallback callback;
/**
* A queue of resources that will be closed when the exchange ends, add more
* resources via {@link #deferClose(Closeable)}.
*/
private final BlockingDeque<Closeable> closables = new LinkedBlockingDeque<>();
private final UndertowEndpoint endpoint;
private final Exchange exchange;
private final ClientRequest request;
private final Boolean throwExceptionOnFailure;
UndertowClientCallback(final Exchange exchange, final AsyncCallback callback, final UndertowEndpoint endpoint,
final ClientRequest request, final ByteBuffer body) {
this.exchange = exchange;
this.callback = callback;
this.endpoint = endpoint;
this.request = request;
this.body = body;
throwExceptionOnFailure = endpoint.getThrowExceptionOnFailure();
}
@Override
public void completed(final ClientConnection connection) {
// we have established connection, make sure we close it
deferClose(connection);
// now we can send the request and perform the exchange: writing the
// request and reading the response
connection.sendRequest(request, on(this::performClientExchange));
}
@Override
public void failed(final IOException e) {
hasFailedWith(e);
}
ChannelListener<StreamSinkChannel> asyncWriter(final ByteBuffer body) {
return channel -> {
try {
write(channel, body);
if (body.hasRemaining()) {
channel.resumeWrites();
} else {
flush(channel);
}
} catch (final IOException e) {
hasFailedWith(e);
}
};
}
void deferClose(final Closeable closeable) {
try {
closables.putFirst(closeable);
} catch (final InterruptedException e) {
hasFailedWith(e);
}
}
void finish(final Message result) {
for (final Closeable closeable : closables) {
IoUtils.safeClose(closeable);
}
if (result != null) {
if (ExchangeHelper.isOutCapable(exchange)) {
exchange.setOut(result);
} else {
exchange.setIn(result);
}
}
callback.done(false);
}
void hasFailedWith(final Throwable e) {
LOG.trace("Exchange has failed with", e);
if (Boolean.TRUE.equals(throwExceptionOnFailure)) {
exchange.setException(e);
}
finish(null);
}
<T> ClientCallback<T> on(final Consumer<T> consumer) {
return new ErrorHandlingClientCallback<>(consumer);
}
void performClientExchange(final ClientExchange clientExchange) {
// add response listener to the exchange, we could receive the response
// at any time (async)
setupResponseListener(clientExchange);
// write the request
writeRequest(clientExchange, body);
}
void setupResponseListener(final ClientExchange clientExchange) {
clientExchange.setResponseListener(on((ClientExchange response) -> {
LOG.trace("completed: {}", clientExchange);
try {
storeCookies(clientExchange);
final UndertowHttpBinding binding = endpoint.getUndertowHttpBinding();
final Message result = binding.toCamelMessage(clientExchange, exchange);
// if there was a http error code then check if we should throw an exception
final int code = clientExchange.getResponse().getResponseCode();
LOG.debug("Http responseCode: {}", code);
final boolean ok = HttpHelper.isStatusCodeOk(code, "200-299");
if (!ok && throwExceptionOnFailure) {
// operation failed so populate exception to throw
final String uri = endpoint.getHttpURI().toString();
final String statusText = clientExchange.getResponse().getStatus();
// Convert Message headers (Map<String, Object>) to Map<String, String> as expected by HttpOperationsFailedException
// using Message versus clientExchange as its header values have extra formatting
final Map<String, String> headers = result.getHeaders().entrySet()
.stream()
.collect(Collectors.toMap(Map.Entry::getKey, (entry) -> entry.getValue().toString()));
// Since result (Message) isn't associated with an Exchange yet, you can not use result.getBody(String.class)
final String bodyText = ExchangeHelper.convertToType(exchange, String.class, result.getBody());
final Exception cause = new HttpOperationFailedException(uri, code, statusText, null, headers, bodyText);
if (ExchangeHelper.isOutCapable(exchange)) {
exchange.setOut(result);
} else {
exchange.setIn(result);
}
// make sure to fail with HttpOperationFailedException
hasFailedWith(cause);
} else {
// we end Camel exchange here
finish(result);
}
} catch (Throwable e) {
hasFailedWith(e);
}
}));
}
void storeCookies(final ClientExchange clientExchange) throws IOException, URISyntaxException {
if (endpoint.getCookieHandler() != null) {
// creating the url to use takes 2-steps
final String url = UndertowHelper.createURL(exchange, endpoint);
final URI uri = UndertowHelper.createURI(exchange, url, endpoint);
final HeaderMap headerMap = clientExchange.getResponse().getResponseHeaders();
final Map<String, List<String>> m = new HashMap<>();
for (final HttpString headerName : headerMap.getHeaderNames()) {
final List<String> headerValue = new LinkedList<>();
for (int i = 0; i < headerMap.count(headerName); i++) {
headerValue.add(headerMap.get(headerName, i));
}
m.put(headerName.toString(), headerValue);
}
endpoint.getCookieHandler().storeCookies(exchange, uri, m);
}
}
void writeRequest(final ClientExchange clientExchange, final ByteBuffer body) {
final StreamSinkChannel requestChannel = clientExchange.getRequestChannel();
if (body != null) {
try {
// try writing, we could be on IO thread and ready to write to
// the socket (or not)
write(requestChannel, body);
if (body.hasRemaining()) {
// we did not write all of body (or at all) register a write
// listener to write asynchronously
requestChannel.getWriteSetter().set(asyncWriter(body));
requestChannel.resumeWrites();
} else {
// we are done, we need to flush the request
flush(requestChannel);
}
} catch (final IOException e) {
hasFailedWith(e);
}
}
}
static void flush(final StreamSinkChannel channel) throws IOException {
// the canonical way of flushing Xnio channels
channel.shutdownWrites();
if (!channel.flush()) {
final ChannelListener<StreamSinkChannel> safeClose = IoUtils::safeClose;
final ChannelExceptionHandler<Channel> closingChannelExceptionHandler = ChannelListeners
.closingChannelExceptionHandler();
final ChannelListener<StreamSinkChannel> flushingChannelListener = ChannelListeners
.flushingChannelListener(safeClose, closingChannelExceptionHandler);
channel.getWriteSetter().set(flushingChannelListener);
channel.resumeWrites();
}
}
static void write(final StreamSinkChannel channel, final ByteBuffer body) throws IOException {
int written = 1;
while (body.hasRemaining() && written > 0) {
written = channel.write(body);
}
}
}