All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.apache.camel.component.undertow.UndertowClientCallback Maven / Gradle / Ivy

There is a newer version: 4.9.0
Show newest version
/*
 * 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.base.HttpOperationFailedException;
import org.apache.camel.support.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)}
 * 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(Throwable)} 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 {

    /**
     * {@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 implements ClientCallback {

        private final Consumer consumer;

        private ErrorHandlingClientCallback(final Consumer 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);

    /**
     * A queue of resources that will be closed when the exchange ends, add more resources via
     * {@link #deferClose(Closeable)}.
     */
    protected final BlockingDeque closables = new LinkedBlockingDeque<>();

    protected final UndertowEndpoint endpoint;
    protected final Exchange exchange;
    protected final ClientRequest request;
    protected final AsyncCallback callback;
    private final ByteBuffer body;
    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;
        this.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 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);
            Thread.currentThread().interrupt();
        }
    }

    protected void finish(final Message result) {
        finish(result, true);
    }

    protected void finish(final Message result, boolean close) {
        if (close) {
            closables.forEach(IoUtils::safeClose);
        }

        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);
        exchange.setException(e);

        finish(null);
    }

    protected  ClientCallback on(final Consumer 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);
    }

    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 = code >= 200 && code <= 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) to Map as expected by HttpOperationsFailedException
                    // using Message versus clientExchange as its header values have extra formatting
                    final Map 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 (Exception 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 Map> m = extractHeaders(clientExchange);
            endpoint.getCookieHandler().storeCookies(exchange, uri, m);
        }
    }

    private static Map> extractHeaders(ClientExchange clientExchange) {
        final HeaderMap headerMap = clientExchange.getResponse().getResponseHeaders();
        final Map> m = new HashMap<>();
        for (final HttpString headerName : headerMap.getHeaderNames()) {
            final List headerValue = new LinkedList<>();
            for (int i = 0; i < headerMap.count(headerName); i++) {
                headerValue.add(headerMap.get(headerName, i));
            }
            m.put(headerName.toString(), headerValue);
        }
        return m;
    }

    protected void writeRequest(final ClientExchange clientExchange) {
        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 safeClose = IoUtils::safeClose;
            final ChannelExceptionHandler closingChannelExceptionHandler = ChannelListeners
                    .closingChannelExceptionHandler();
            final ChannelListener 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);
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy