Java tutorial
/* * Copyright (c) 2013-2015 the original author or authors * * 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.werval.server.netty; import io.netty.buffer.ByteBufHolder; import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelFutureListener; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.SimpleChannelInboundHandler; import io.netty.handler.codec.http.DefaultFullHttpResponse; import io.netty.handler.codec.http.DefaultHttpResponse; import io.netty.handler.codec.http.FullHttpRequest; import io.netty.handler.codec.http.HttpResponse; import io.netty.handler.codec.http.HttpResponseStatus; import io.netty.handler.codec.http.HttpVersion; import io.netty.handler.stream.ChunkedStream; import io.netty.handler.timeout.ReadTimeoutException; import io.netty.handler.timeout.WriteTimeoutException; import io.werval.api.Mode; import io.werval.api.events.HttpEvent; import io.werval.api.http.ProtocolVersion; import io.werval.api.http.Request; import io.werval.api.http.RequestHeader; import io.werval.api.http.ResponseHeader; import io.werval.api.http.Status; import io.werval.api.outcomes.Outcome; import io.werval.runtime.outcomes.ChunkedInputOutcome; import io.werval.runtime.outcomes.InputStreamOutcome; import io.werval.runtime.outcomes.SimpleOutcome; import io.werval.spi.ApplicationSPI; import io.werval.spi.dev.DevShellRebuildException; import io.werval.spi.dev.DevShellSPI; import java.io.IOException; import java.io.InputStream; import java.io.UncheckedIOException; import java.math.BigDecimal; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import static io.netty.handler.codec.http.HttpResponseStatus.BAD_REQUEST; import static io.netty.handler.codec.http.HttpResponseStatus.INTERNAL_SERVER_ERROR; import static io.netty.handler.codec.http.HttpVersion.HTTP_1_1; import static io.werval.api.http.Headers.Names.CONTENT_LENGTH; import static io.werval.api.http.Headers.Names.TRAILER; import static io.werval.api.http.Headers.Names.TRANSFER_ENCODING; import static io.werval.api.http.Headers.Names.X_WERVAL_CONTENT_LENGTH; import static io.werval.api.http.Headers.Values.CHUNKED; import static io.werval.util.Charsets.UTF_8; import static io.werval.server.netty.NettyHttpFactories.remoteAddressOf; import static io.werval.server.netty.NettyHttpFactories.requestOf; /** * Handle HTTP Requests. * * Any HTTP request message is allowed to contain a message body, and thus must be parsed with that in mind. * This implementation consume the request body for any requests methods but it is only parsed for POST, PUT * and PATCH methods. Parsing is done only for URL-encoded forms and multipart form data. For other request body * types, it's the application responsibility to do the parsing. */ // TODO WebSocket UPGRADE public final class WervalHttpHandler extends SimpleChannelInboundHandler<FullHttpRequest> { private static final Logger LOG = LoggerFactory.getLogger(WervalHttpHandler.class); private final class HttpRequestCompleteChannelFutureListener implements ChannelFutureListener { private final RequestHeader requestHeader; private HttpRequestCompleteChannelFutureListener(RequestHeader requestHeader) { this.requestHeader = requestHeader; } @Override public void operationComplete(ChannelFuture future) throws Exception { if (future.isSuccess()) { LOG.trace("{} Request completed successfully", requestIdentity); app.onHttpRequestComplete(requestHeader); } } } private final ApplicationSPI app; private final DevShellSPI devSpi; private String requestIdentity; private RequestHeader requestHeader; public WervalHttpHandler(ApplicationSPI app, DevShellSPI devSpi) { super(); this.app = app; this.devSpi = devSpi; } @Override protected void channelRead0(ChannelHandlerContext nettyContext, FullHttpRequest nettyRequest) throws Exception { // Get the request unique identifier requestIdentity = nettyContext.channel().attr(Attrs.REQUEST_IDENTITY).get(); assert requestIdentity != null; if (LOG.isTraceEnabled()) { LOG.trace("{} Received a FullHttpRequest:\n{}", requestIdentity, nettyRequest.toString()); } // Return 503 to incoming requests while shutting down if (nettyContext.executor().isShuttingDown()) { app.shuttingDownOutcome(ProtocolVersion.valueOf(nettyRequest.getProtocolVersion().text()), requestIdentity).thenAcceptAsync(shuttingDownOutcome -> { writeOutcome(nettyContext, shuttingDownOutcome).addListeners( new HttpRequestCompleteChannelFutureListener(requestHeader), f -> app.events().emit(new HttpEvent.ResponseSent(requestIdentity, shuttingDownOutcome.responseHeader().status()))); }, app.executor()); return; } // In development mode, rebuild application source if needed if (devSpi != null && devSpi.isSourceChanged()) { devSpi.rebuild(); } // Create Request Instance // Can throw HttpRequestParsingException Request request = requestOf(app.defaultCharset(), app.httpBuilders(), remoteAddressOf(nettyContext.channel()), requestIdentity, nettyRequest); requestHeader = request; // Handle Request app.handleRequest(request).thenAcceptAsync(outcome -> { // Write Outcome ChannelFuture writeFuture = writeOutcome(nettyContext, outcome); // Listen to request completion writeFuture.addListeners( f -> app.events() .emit(new HttpEvent.ResponseSent(requestIdentity, outcome.responseHeader().status())), new HttpRequestCompleteChannelFutureListener(requestHeader)); }, app.executor()); } @Override public void exceptionCaught(ChannelHandlerContext nettyContext, Throwable cause) throws IOException { if (cause instanceof ReadTimeoutException) { LOG.trace("{} Read timeout, connection has been closed.", requestIdentity); } else if (cause instanceof WriteTimeoutException) { LOG.trace("{} Write timeout, connection has been closed.", requestIdentity); } else if (cause instanceof DevShellRebuildException) { byte[] htmlErrorPage = ((DevShellRebuildException) cause).htmlErrorPage().getBytes(UTF_8); DefaultFullHttpResponse nettyResponse = new DefaultFullHttpResponse(HTTP_1_1, INTERNAL_SERVER_ERROR); nettyResponse.headers().set(CONTENT_LENGTH, htmlErrorPage.length); ((ByteBufHolder) nettyResponse).content().writeBytes(htmlErrorPage); nettyContext.writeAndFlush(nettyResponse).addListeners( f -> app.events() .emit(new HttpEvent.ResponseSent(requestIdentity, Status.INTERNAL_SERVER_ERROR)), new HttpRequestCompleteChannelFutureListener(requestHeader), ChannelFutureListener.CLOSE); } else if (requestHeader != null) { // Write Outcome Outcome errorOutcome = app.handleError(requestHeader, cause); ChannelFuture writeFuture = writeOutcome(nettyContext, errorOutcome); // Listen to request completion writeFuture .addListeners( f -> app.events() .emit(new HttpEvent.ResponseSent(requestIdentity, errorOutcome.responseHeader().status())), new HttpRequestCompleteChannelFutureListener(requestHeader)); } else if (cause instanceof HttpRequestParsingException) { if (app.mode() == Mode.PROD) { LOG.trace("HTTP request parsing error, returning 400, was: {}", cause.getMessage(), cause); } else { LOG.warn("HTTP request parsing error, returning 400, was: {}", cause.getMessage(), cause); } DefaultFullHttpResponse nettyResponse = new DefaultFullHttpResponse(HTTP_1_1, BAD_REQUEST); nettyResponse.headers().set(CONTENT_LENGTH, 0); nettyContext.writeAndFlush(nettyResponse).addListeners( f -> app.events().emit(new HttpEvent.ResponseSent(requestIdentity, Status.BAD_REQUEST)), new HttpRequestCompleteChannelFutureListener(requestHeader), ChannelFutureListener.CLOSE); } else { LOG.error( "HTTP Server encountered an unexpected error, please raise an issue with the complete stacktrace", cause); nettyContext.close(); } } private ChannelFuture writeOutcome(ChannelHandlerContext nettyContext, Outcome outcome) { // == Build the Netty Response ResponseHeader responseHeader = outcome.responseHeader(); // Netty Version & Status HttpVersion responseVersion = HttpVersion.valueOf(responseHeader.version().toString()); HttpResponseStatus responseStatus = HttpResponseStatus.valueOf(responseHeader.status().code()); // Netty Headers & Body output final HttpResponse nettyResponse; final ChannelFuture writeFuture; if (outcome instanceof ChunkedInputOutcome) { ChunkedInputOutcome chunkedOutcome = (ChunkedInputOutcome) outcome; nettyResponse = new DefaultHttpResponse(responseVersion, responseStatus); // Headers applyResponseHeader(responseHeader, nettyResponse); nettyResponse.headers().set(TRANSFER_ENCODING, CHUNKED); nettyResponse.headers().set(TRAILER, X_WERVAL_CONTENT_LENGTH); // Body nettyContext.write(nettyResponse); writeFuture = nettyContext.writeAndFlush(new HttpChunkedBodyEncoder( new ChunkedStream(chunkedOutcome.inputStream(), chunkedOutcome.chunkSize()))); } else if (outcome instanceof InputStreamOutcome) { InputStreamOutcome streamOutcome = (InputStreamOutcome) outcome; nettyResponse = new DefaultFullHttpResponse(responseVersion, responseStatus); // Headers applyResponseHeader(responseHeader, nettyResponse); nettyResponse.headers().set(CONTENT_LENGTH, streamOutcome.contentLength()); // Body try (InputStream bodyInputStream = streamOutcome.bodyInputStream()) { ((ByteBufHolder) nettyResponse).content().writeBytes(bodyInputStream, new BigDecimal(streamOutcome.contentLength()).intValueExact()); } catch (IOException ex) { throw new UncheckedIOException(ex); } writeFuture = nettyContext.writeAndFlush(nettyResponse); } else if (outcome instanceof SimpleOutcome) { SimpleOutcome simpleOutcome = (SimpleOutcome) outcome; byte[] body = simpleOutcome.body().asBytes(); nettyResponse = new DefaultFullHttpResponse(responseVersion, responseStatus); // Headers applyResponseHeader(responseHeader, nettyResponse); nettyResponse.headers().set(CONTENT_LENGTH, body.length); // Body ((ByteBufHolder) nettyResponse).content().writeBytes(body); writeFuture = nettyContext.writeAndFlush(nettyResponse); } else { LOG.warn("{} Unhandled Outcome type '{}', no response body.", requestIdentity, outcome.getClass()); nettyResponse = new DefaultFullHttpResponse(responseVersion, responseStatus); applyResponseHeader(responseHeader, nettyResponse); writeFuture = nettyContext.writeAndFlush(nettyResponse); } if (LOG.isTraceEnabled()) { LOG.trace("{} Sent a HttpResponse:\n{}", requestIdentity, nettyResponse.toString()); } // Close the connection as soon as the response is sent if not keep alive if (!outcome.responseHeader().isKeepAlive() || nettyContext.executor().isShuttingDown()) { writeFuture.addListener(ChannelFutureListener.CLOSE); } // Done! return writeFuture; } /** * Apply Headers and Cookies into Netty HttpResponse. * * @param response Werval ResponseHeader * @param nettyResponse Netty HttpResponse */ private void applyResponseHeader(ResponseHeader response, HttpResponse nettyResponse) { for (String name : response.headers().keys()) { nettyResponse.headers().add(name, response.headers().values(name)); } } }