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

io.inverno.mod.http.server.internal.AbstractExchange Maven / Gradle / Ivy

There is a newer version: 1.12.0
Show newest version
/*
 * Copyright 2020 Jeremy KUHN
 *
 * Licensed 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 io.inverno.mod.http.server.internal;

import io.inverno.mod.http.base.ExchangeContext;
import io.inverno.mod.http.base.HttpException;
import io.inverno.mod.http.base.Method;
import io.inverno.mod.http.base.header.Headers;
import io.inverno.mod.http.server.ErrorExchange;
import io.inverno.mod.http.server.Exchange;
import io.inverno.mod.http.server.ServerController;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelPromise;
import io.netty.util.concurrent.EventExecutor;
import java.net.InetSocketAddress;
import org.apache.commons.text.StringEscapeUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.Marker;
import org.apache.logging.log4j.MarkerManager;
import org.apache.logging.log4j.message.MultiformatMessage;
import org.apache.logging.log4j.util.Strings;
import org.reactivestreams.Subscription;
import reactor.core.Disposable;
import reactor.core.publisher.BaseSubscriber;
import reactor.core.publisher.Mono;
import reactor.core.publisher.SignalType;

/**
 * 

* Base {@link Exchange} implementation. *

* *

* This class also implements the subscriber used to subscribe to exchange response data publisher. *

* *

* Implementors must provide the implementation of methods that actually send response data to the client. *

* * @author Jeremy Kuhn * @since 1.0 */ public abstract class AbstractExchange extends BaseSubscriber implements Exchange { private static final Logger LOGGER = LogManager.getLogger(Exchange.class); private static final Marker MARKER_ERROR = MarkerManager.getMarker("HTTP_ERROR"); private static final Marker MARKER_ACCESS = MarkerManager.getMarker("HTTP_ACCESS"); protected final ChannelHandlerContext context; protected final EventExecutor contextExecutor; protected final ServerController, ErrorExchange> controller; protected final AbstractRequest request; protected AbstractResponse response; protected final ExchangeContext exchangeContext; protected Mono finalizer; protected Handler handler; protected int transferedLength; protected boolean single; protected boolean many; private ByteBuf singleChunk; /** * the current disposable */ protected Disposable disposable; protected static final ServerController, ErrorExchange> LAST_RESORT_ERROR_CONTROLLER = exchange -> {}; /** *

* Creates a server exchange with the specified channel handler context, root exchange handler, error exchange handler, request and response. *

* * @param context the channel handler context * @param controller the server controller * @param request the exchange request * @param response the exchange response */ public AbstractExchange( ChannelHandlerContext context, ServerController, ErrorExchange> controller, AbstractRequest request, AbstractResponse response ) { this.context = context; this.contextExecutor = this.context.executor(); this.controller = controller; this.request = request; this.response = response; this.exchangeContext = controller.createContext(); if(this.exchangeContext != null) { this.exchangeContext.init(); } } @Override public AbstractRequest request() { return this.request; } @Override public AbstractResponse response() { return this.response; } @Override public ExchangeContext context() { return this.exchangeContext; } @Override public void finalizer(Mono finalizer) { if(this.finalizer != null) { this.finalizer = this.finalizer.then(finalizer); } else { this.finalizer = finalizer; } } /** *

* Finalizes the exchange by invoking the finalizer and the postFinalize when the final promise completes when a finalizer has been provided, otherwise the postFinalize runnable is invoked * immediately. *

* *

* When using a finalizer, we have to wait for the final write operation to complete before invoking the finalizer, this basically breaks HTTP pipelining but this is mandatory to get a chance to * reset shared resources used to process multiple exchanges (eg. Bytebuf). *

* * @param finalPromise a promise that completes with the final exchange operation * @param postFinalize a post finalize operation or null * * @return the promise */ public ChannelFuture finalizeExchange(ChannelPromise finalPromise, Runnable postFinalize) { if(this.finalizer != null) { finalPromise.addListener(future -> { Mono actualFinalizer = this.finalizer; if(postFinalize != null) { actualFinalizer.doOnTerminate(postFinalize); } actualFinalizer.doOnSuccess(ign -> LOGGER.trace(() -> "Exchange finalized")).subscribe(); }); } else if(postFinalize != null){ postFinalize.run(); } return finalPromise; } /** *

* Returns the controller. *

* * @return the root handler */ public ServerController, ErrorExchange> getController() { return this.controller; } /** *

* Returns the current transfered content length. *

* * @return the current transfered content length */ public long getTransferedLength() { return this.transferedLength; } @Override public void dispose() { if(this.disposable == this) { super.dispose(); } else if(this.disposable != null) { this.disposable.dispose(); } this.request.dispose(); } @Override public boolean isDisposed() { return this.disposable != null ? this.disposable.isDisposed() : true; } /** *

* Starts the processing of the exchange with the specified callback handler. *

* *

* This methods invokes the server root handler on the exchange and subscribe to the response data publisher. *

* * @param handler an exchange callback handler */ public void start(Handler handler) { if(this.handler != null) { throw new IllegalStateException("Exchange already started"); } this.handler = handler; this.handler.exchangeStart(this.context, this); Mono deferHandle; try { deferHandle = this.controller.defer(this); } catch(Throwable throwable) { this.logError("Exchange handler error", throwable); // We need to create a new error exchange each time we try to handle the error in order to have a fresh response ErrorExchange errorExchange = this.createErrorExchange(throwable); this.response = (AbstractResponse) errorExchange.response(); try { deferHandle = this.controller.defer(errorExchange); } catch (Throwable t) { this.logError("ErrorExchange handler error", t); errorExchange = this.createErrorExchange(throwable); this.response = (AbstractResponse) errorExchange.response(); // TODO This may fail as well what do we do in such situations? deferHandle = LAST_RESORT_ERROR_CONTROLLER.defer(errorExchange); } } ServerControllerSubscriber subscriber = this.createServerControllerSubscriber(); this.disposable = subscriber; deferHandle.subscribe(subscriber); } /** *

* Creates the server controller subscriber used to consume the exchange deferred handle Mono supplied by {@link ServerController#defer(Exchange)}. *

* * @return the server controller subscriber */ protected ServerControllerSubscriber createServerControllerSubscriber() { return new ServerControllerSubscriber(); } /** *

* Executes the specified task in the event loop. *

* *

* The tasks is executed immediately when the current thread is in the event loop, otherwise it is scheduled in the event loop. *

* *

* After the execution of the task, one event is requested to the response data subscriber. *

* * @param runnable the task to execute */ protected void executeInEventLoop(Runnable runnable) { this.executeInEventLoop(runnable, 1); } /** *

* Executes the specified task in the event loop. *

* *

* The tasks is executed immediately when the current thread is in the event loop, otherwise it is scheduled in the event loop. *

* *

* After the execution of the task, the specified number of events is requested to the response data subscriber. *

* * @param runnable the task to execute * @param request the number of events to request to the response data subscriber after the task completes */ protected void executeInEventLoop(Runnable runnable, int request) { if(this.contextExecutor.inEventLoop()) { runnable.run(); this.request(request); } else { this.contextExecutor.execute(() -> { try { runnable.run(); this.request(request); } catch (Throwable throwable) { this.cancel(); this.hookOnError(throwable); } }); } } /** *

* Creates an error exchange handler from the exchange with the specified error. *

* * @param error the error * * @return a new error exchange based on the exchange */ protected abstract ErrorExchange createErrorExchange(Throwable error); @Override protected final void hookOnSubscribe(Subscription subscription) { this.onStart(subscription); } /** *

* Invokes when the exchange is started. *

* *

* The default implementation basically request an unbounded amount of events to the subscription. *

* * @param subscription the subscription to the response data publisher */ protected void onStart(Subscription subscription) { subscription.request(Long.MAX_VALUE); LOGGER.debug(() -> "Exchange started"); } @Override protected final void hookOnNext(ByteBuf value) { this.transferedLength += value.readableBytes(); if( (this.single || !this.many) && this.singleChunk == null) { // either we know we have a mono or we don't know yet if we have many this.singleChunk = value; } else { // We don't have a mono and we know we have multiple chunks this.many = true; final ByteBuf firstValue = this.singleChunk; this.singleChunk = null; this.executeInEventLoop(() -> { if(firstValue != null) { this.onNextMany(firstValue); } this.onNextMany(value); }); } } /** *

* Invokes on an event when the response data publisher emits more than one event. *

* * @param value the event data */ protected abstract void onNextMany(ByteBuf value); /** *

* Invokes when the response data publisher completes with an error *

*/ @Override protected final void hookOnError(Throwable throwable) { // if headers are already written => close the connection nothing we can do // if headers are not already written => we should invoke the error handler // what we need is to continue processing // - create a new Response for the error // - reset this exchange => transferedLength, chunkCount must be reset // - invoke the error handler (potentially the fallback error handler) with a new ErrorExchange if(this.response.isHeadersWritten()) { this.executeInEventLoop(() -> { this.onCompleteWithError(throwable); this.logError("Exchange processing error", throwable); }); } else { this.transferedLength = 0; ErrorExchange errorExchange = this.createErrorExchange(throwable); this.response = (AbstractResponse) errorExchange.response(); try { Mono deferHandle = this.controller.defer(errorExchange); this.executeInEventLoop(() -> { ErrorHandlerSubscriber subscriber = new ErrorHandlerSubscriber(throwable); this.disposable = subscriber; deferHandle.subscribe(subscriber); }); } catch (Throwable t) { this.logError("ErrorExchange handler error", t); errorExchange = this.createErrorExchange(throwable); this.response = (AbstractResponse) errorExchange.response(); // TODO This may fail as well what do we do in such situations? Mono deferHandle = LAST_RESORT_ERROR_CONTROLLER.defer(errorExchange); this.executeInEventLoop(() -> { ErrorHandlerSubscriber subscriber = new ErrorHandlerSubscriber(throwable); this.disposable = subscriber; deferHandle.subscribe(subscriber); }); } } } /** *

* Invokes when the response data stream completes with error. *

* * @param throwable the error */ protected abstract void onCompleteWithError(Throwable throwable); @Override protected final void hookOnComplete() { if(this.transferedLength == 0) { if(this.response.headers().getCharSequence(Headers.NAME_CONTENT_LENGTH) == null) { this.response.headers().contentLength(0); } this.executeInEventLoop(() -> { this.onCompleteEmpty(); this.logAccess(); }); } else if(this.singleChunk != null) { // single chunk response if(this.response.headers().getCharSequence(Headers.NAME_CONTENT_LENGTH) == null) { this.response.headers().contentLength(this.transferedLength); } if(this.request.getMethod().equals(Method.HEAD)) { this.executeInEventLoop(() -> { this.onCompleteEmpty(); this.logAccess(); }); } else { this.executeInEventLoop(() -> { this.onCompleteSingle(this.singleChunk); this.logAccess(); }); } } else { this.executeInEventLoop(() -> { this.onCompleteMany(); this.logAccess(); }); } LOGGER.debug(() -> "Exchange completed"); } /** *

* Logs an access log message. *

*/ protected void logAccess() { LOGGER.info(MARKER_ACCESS, () -> new AccessLogMessage()); } /** *

* Access log message *

* * @author Jeremy Kuhn * @since 1.1.1 */ private class AccessLogMessage implements MultiformatMessage { private static final long serialVersionUID = -8367544116216876788L; private static final String JSON_FORMAT = "JSON"; @Override public String getFormat() { return Strings.EMPTY; } @Override public Object[] getParameters() { return new Object[] { this.getRemoteAddress(), this.getRequest(), this.getStatus(), this.getTransferedBytes(), this.getReferer(), this.getUserAgent() }; } @Override public Throwable getThrowable() { return null; } @Override public String getFormattedMessage() { return this.asString(); } @Override public String getFormattedMessage(String[] formats) { for(String format : formats) { if(format.equalsIgnoreCase(JSON_FORMAT)) { return this.asJson(); } } return this.asString(); } @Override public String[] getFormats() { return new String[] { "JSON" }; } private String getRemoteAddress() { return ((InetSocketAddress)AbstractExchange.this.request.getRemoteAddress()).getAddress().getHostAddress(); } private String getRequest() { return new StringBuilder().append(AbstractExchange.this.request.getMethod().name()).append(" ").append(AbstractExchange.this.request.getPath()).toString(); } private int getStatus() { return AbstractExchange.this.response.headers().getStatusCode(); } private int getTransferedBytes() { return AbstractExchange.this.transferedLength; } private String getReferer() { return AbstractExchange.this.request.headers().get(Headers.NAME_REFERER).orElse(""); } private String getUserAgent() { return AbstractExchange.this.request.headers().get(Headers.NAME_USER_AGENT).orElse(""); } private String asString() { StringBuilder message = new StringBuilder(); message.append(((InetSocketAddress)AbstractExchange.this.request.getRemoteAddress()).getAddress().getHostName()).append(" "); message.append("\"").append(AbstractExchange.this.request.getMethod().name()).append(" ").append(AbstractExchange.this.request.getPath()).append("\" "); message.append(AbstractExchange.this.response.headers().getStatusCode()).append(" "); message.append(AbstractExchange.this.transferedLength).append(" "); message.append("\"").append(AbstractExchange.this.request.headers().get(Headers.NAME_REFERER).orElse("")).append("\" "); message.append("\"").append(AbstractExchange.this.request.headers().get(Headers.NAME_USER_AGENT).orElse("")).append("\" "); return message.toString(); } private String asJson() { StringBuilder message = new StringBuilder(); message.append("{"); message.append("\"remoteAddress\":\"").append(this.getRemoteAddress()).append("\","); message.append("\"request\":\"").append(StringEscapeUtils.escapeJson(this.getRequest())).append("\","); message.append("\"status\":").append(this.getStatus()).append(","); message.append("\"bytes\":").append(this.getTransferedBytes()).append(","); message.append("\"referer\":\"").append(StringEscapeUtils.escapeJson(this.getReferer())).append("\","); message.append("\"userAgent\":\"").append(StringEscapeUtils.escapeJson(this.getUserAgent())).append("\""); message.append("}"); return message.toString(); } } /** *

* Logs an error. *

* * @param message the message * @param throwable the error */ private void logError(String message, Throwable throwable) { if(throwable instanceof HttpException) { // HTTP error: typically recoverable HTTP exceptions returning a proper error to the client LOGGER.error(MARKER_ERROR, message, throwable); } else { // non HTTP error: typically unrecoverable unchecked exceptions LOGGER.error(message, throwable); } } /** *

* Invoked when the response data publisher completes with no data. *

*/ protected abstract void onCompleteEmpty(); /** *

* Invoked when the response data publisher completes with a single data. *

* * @param value the single byte buffer */ protected abstract void onCompleteSingle(ByteBuf value); /** *

* Invoked when the response data publisher completes with many data. *

*/ protected abstract void onCompleteMany(); @Override protected void hookFinally(SignalType type) { this.request.dispose(); } /** *

* Exchange callbacks handler. *

* * @author Jeremy Kuhn * @since 1.0 * * @see AbstractExchange#start(Handler) */ public static interface Handler { /** * Default handler. */ static Handler DEFAULT = new Handler() {}; /** *

* Notifies that the exchange has started. *

* * @param ctx the channel handler context * @param exchange the exchange */ default void exchangeStart(ChannelHandlerContext ctx, AbstractExchange exchange) { } /** *

* Notifies request data was received. *

* * @param ctx the channel handler context. * @param t the received data */ default void exchangeNext(ChannelHandlerContext ctx, ByteBuf t) { } /** *

* Notifies that an error was raised during the processing of the exchange. *

* * @param ctx the chanel handler context * @param t an error */ default void exchangeError(ChannelHandlerContext ctx, Throwable t) { this.exchangeComplete(ctx); } /** *

* Notifies that the exchange has completed. *

* *

* This means the response has been fully sent. *

* * @param ctx the channel handler context */ default void exchangeComplete(ChannelHandlerContext ctx) { } } /** *

* An error subscriber which is created to subscribe to the response data publisher of the error exchange created when the response data publisher completes with an error. *

* * @author Jeremy Kuhn * @since 1.0 */ private final class ErrorSubscriber extends BaseSubscriber { private final Throwable originalError; public ErrorSubscriber(Throwable originalError) { this.originalError = originalError; } @Override protected void hookOnNext(ByteBuf value) { AbstractExchange.this.hookOnNext(value); } @Override protected void hookOnError(Throwable throwable) { // If we get there it means we can no longer process anything AbstractExchange.this.onCompleteWithError(this.originalError); AbstractExchange.this.logError("Exchange processing error", this.originalError); AbstractExchange.this.logError("ErrorExchange processing error", throwable); } @Override protected void hookOnComplete() { AbstractExchange.this.hookOnComplete(); AbstractExchange.this.logError("Exchange processing error", this.originalError); AbstractExchange.this.logAccess(); } } /** *

* A subscriber to consume the exchange deferred handle Mono supplied by {@link ServerController#defer(Exchange)}. On complete it subscribes to the exchange response data publisher. *

* * @author Jeremy Kuhn * @since 1.3 */ protected class ServerControllerSubscriber extends BaseSubscriber { @Override protected void hookOnError(Throwable t) { AbstractExchange.this.hookOnError(t); } @Override protected void hookOnComplete() { if(AbstractExchange.this.request.getMethod().equals(Method.HEAD)) { AbstractExchange.this.executeInEventLoop(AbstractExchange.this::onCompleteEmpty); AbstractExchange.this.dispose(); } else { AbstractExchange.this.single = AbstractExchange.this.response.isSingle(); AbstractExchange.this.disposable = AbstractExchange.this; AbstractExchange.this.response.dataSubscribe(AbstractExchange.this); } } } /** *

* An subscriber to consume the error exchange deferred handle Mono supplied by {@link ErrorExchangeHandler#defer(Exchange)}. On complete it uses the {@link AbstractExchange#errorSubscriber} to * subscribe to the error exchange response data publisher. *

* * @author Jeremy Kuhn * @since 1.3 */ private final class ErrorHandlerSubscriber extends BaseSubscriber { private final Throwable originalError; public ErrorHandlerSubscriber(Throwable originalError) { this.originalError = originalError; } @Override protected void hookOnError(Throwable t) { AbstractExchange.this.hookOnError(t); } @Override protected void hookOnComplete() { AbstractExchange.this.single = AbstractExchange.this.response.isSingle(); ErrorSubscriber subscriber = new ErrorSubscriber(this.originalError); AbstractExchange.this.disposable = subscriber; AbstractExchange.this.response.dataSubscribe(subscriber); } } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy