Java tutorial
/* * Copyright 2014 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 org.nosceon.titanite; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import io.netty.channel.*; import io.netty.handler.codec.http.*; import io.netty.handler.stream.ChunkedFile; import java.io.*; import java.net.URI; import java.util.Date; import java.util.Locale; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionStage; import static io.netty.handler.codec.http.HttpHeaders.Names.*; import static io.netty.handler.codec.http.HttpHeaders.*; import static io.netty.handler.codec.http.HttpVersion.HTTP_1_1; import static io.netty.util.CharsetUtil.UTF_8; import static java.util.concurrent.CompletableFuture.completedFuture; import static org.nosceon.titanite.Exceptions.internalServerError; /** * @author Johan Siebens */ public final class Response { public static Response status(int status) { return new Response(HttpResponseStatus.valueOf(status)); } public static Response ok() { return new Response(HttpResponseStatus.OK); } public static Response created(String location) { return new Response(HttpResponseStatus.CREATED).location(location); } public static Response created(URI location) { return new Response(HttpResponseStatus.CREATED).location(location); } public static Response accepted() { return new Response(HttpResponseStatus.ACCEPTED); } public static Response noContent() { return new Response(HttpResponseStatus.NO_CONTENT); } public static Response seeOther(String location) { return new Response(HttpResponseStatus.SEE_OTHER).location(location); } public static Response seeOther(URI location) { return new Response(HttpResponseStatus.SEE_OTHER).location(location); } public static Response temporaryRedirect(String location) { return new Response(HttpResponseStatus.TEMPORARY_REDIRECT).location(location); } public static Response temporaryRedirect(URI location) { return new Response(HttpResponseStatus.TEMPORARY_REDIRECT).location(location); } public static Response notModified() { return new Response(HttpResponseStatus.NOT_MODIFIED); } public static Response badRequest() { return new Response(HttpResponseStatus.BAD_REQUEST); } public static Response unauthorized() { return new Response(HttpResponseStatus.UNAUTHORIZED); } public static Response forbidden() { return new Response(HttpResponseStatus.FORBIDDEN); } public static Response notFound() { return new Response(HttpResponseStatus.NOT_FOUND); } public static Response notAcceptable() { return new Response(HttpResponseStatus.NOT_ACCEPTABLE); } public static Response methodNotAllowed() { return new Response(HttpResponseStatus.METHOD_NOT_ALLOWED); } public static Response conflict() { return new Response(HttpResponseStatus.CONFLICT); } public static Response unsupportedMediaType() { return new Response(HttpResponseStatus.UNSUPPORTED_MEDIA_TYPE); } public static Response notImplemented() { return new Response(HttpResponseStatus.NOT_IMPLEMENTED); } public static CompletionStage<Response> webSocket(WebSocket ws) { return ok().body(new WebSocketBody(ws)).toFuture(); } private HttpResponseStatus status; private HttpHeaders headers = new DefaultHttpHeaders(); private Body body = new NoBody(); public Response(int status) { this(HttpResponseStatus.valueOf(status)); } Response(HttpResponseStatus status) { this.status = status; } public int status() { return status.code(); } public Response header(String name, Object value) { headers.add(name, value); return this; } public Response location(String location) { return location(URI.create(location)); } public Response location(URI location) { headers.set(LOCATION, location); return this; } public Response type(MediaType type) { headers.set(CONTENT_TYPE, type.toString()); return this; } public Response language(Locale language) { headers.set(CONTENT_LANGUAGE, language); return this; } public Response lastModified(Date date) { headers.set(LAST_MODIFIED, date); return this; } public Response cookie(String name, String value) { headers.add(SET_COOKIE, new Cookie(name, value).encode()); return this; } public Response cookie(Cookie cookie) { headers.add(SET_COOKIE, cookie.encode()); return this; } public Response body(String content) { this.body = new DefaultBody(Unpooled.copiedBuffer(content, UTF_8)); return this; } public Response body(InputStream in) { return body(o -> Utils.copy(in, o)); } public Response body(BodyWriter bodyWriter) { this.body = new StreamBody(bodyWriter); return this; } public Response body(File file) { this.body = new FileBody(file); return this; } public Response text(String content) { this.type(MediaType.TEXT_PLAIN); this.body = new DefaultBody(Unpooled.copiedBuffer(content, UTF_8)); return this; } public Response html(String content) { this.type(MediaType.TEXT_HTML); this.body = new DefaultBody(Unpooled.copiedBuffer(content, UTF_8)); return this; } public Response chunks(ChunkedOutput chunkedOutput) { this.body = new ChunkedBody(chunkedOutput); return this; } private Response body(Body body) { this.body = body; return this; } public CompletionStage<Response> toFuture() { return completedFuture(this); } void apply(HttpRequest rawRequest, WebsocketHandler websocketHandler, boolean keepAlive, Request request, ChannelHandlerContext ctx) { body.apply(rawRequest, websocketHandler, request, ctx); } private static interface Body { void apply(HttpRequest rawRequest, WebsocketHandler websocketHandler, Request request, ChannelHandlerContext ctx); } private static void writeFlushAndClose(ChannelHandlerContext ctx, Object msg, boolean keepAlive) { ChannelFuture future = ctx.writeAndFlush(msg); if (!keepAlive) { future.addListener(ChannelFutureListener.CLOSE); } } private class NoBody implements Body { @Override public void apply(HttpRequest rawRequest, WebsocketHandler websocketHandler, Request request, ChannelHandlerContext ctx) { FullHttpResponse response = new DefaultFullHttpResponse(HTTP_1_1, status, Unpooled.EMPTY_BUFFER); response.headers().add(headers); setKeepAlive(response, isKeepAlive(rawRequest)); writeFlushAndClose(ctx, response, false); } } private class DefaultBody implements Body { private ByteBuf content; private DefaultBody(ByteBuf content) { this.content = content; } @Override public void apply(HttpRequest rawRequest, WebsocketHandler websocketHandler, Request request, ChannelHandlerContext ctx) { boolean isHeadRequest = request.method().equals(Method.HEAD); FullHttpResponse response = new DefaultFullHttpResponse(HTTP_1_1, status, isHeadRequest ? Unpooled.EMPTY_BUFFER : content); response.headers().add(headers); setContentLength(response, content.readableBytes()); setKeepAlive(response, isKeepAlive(rawRequest)); writeFlushAndClose(ctx, response, false); } } private static class WebSocketBody implements Body { private final WebSocket webSocket; private WebSocketBody(WebSocket webSocket) { this.webSocket = webSocket; } @Override public void apply(HttpRequest rawRequest, WebsocketHandler websocketHandler, Request request, ChannelHandlerContext ctx) { websocketHandler.handshake(rawRequest, request, ctx, webSocket); } } private class ChunkedBody implements Body { private ChunkedOutput chunkedOutput; private ChunkedBody(ChunkedOutput chunkedOutput) { this.chunkedOutput = chunkedOutput; } @Override public void apply(HttpRequest rawRequest, WebsocketHandler websocketHandler, Request request, ChannelHandlerContext ctx) { HttpResponse response = new DefaultHttpResponse(HTTP_1_1, status); response.headers().add(headers); setTransferEncodingChunked(response); ctx.writeAndFlush(response); if (!request.method().equals(Method.HEAD)) { ChunksChannel channel = new ChunksChannel(isKeepAlive(rawRequest), ctx); ctx.pipeline().addLast(channel); chunkedOutput.onReady(channel); } else { writeFlushAndClose(ctx, LastHttpContent.EMPTY_LAST_CONTENT, isKeepAlive(rawRequest)); } } } private static class ChunksChannel extends ChannelInboundHandlerAdapter implements ChunkedOutput.Channel { private final boolean keepAlive; private final ChannelHandlerContext ctx; private final CompletableFuture<Void> disconnect; private ChunksChannel(boolean keepAlive, ChannelHandlerContext ctx) { this.keepAlive = keepAlive; this.ctx = ctx; this.disconnect = new CompletableFuture<>(); } @Override public void channelInactive(ChannelHandlerContext ctx) throws Exception { disconnect.complete(null); } @Override public void write(byte[] chunk) { ctx.writeAndFlush(new DefaultHttpContent(Unpooled.copiedBuffer(chunk))); } @Override public void close() { ctx.pipeline().remove(this); writeFlushAndClose(ctx, LastHttpContent.EMPTY_LAST_CONTENT, keepAlive); } @Override public void onDisconnect(Runnable listener) { disconnect.whenComplete((v, t) -> listener.run()); } } private class StreamBody implements Body { private BodyWriter consumer; private StreamBody(BodyWriter consumer) { this.consumer = consumer; } @Override public void apply(HttpRequest rawRequest, WebsocketHandler websocketHandler, Request request, ChannelHandlerContext ctx) { HttpResponse response = new DefaultHttpResponse(HTTP_1_1, status); response.headers().add(headers); setTransferEncodingChunked(response); ctx.write(response); if (!request.method().equals(Method.HEAD)) { Utils.runUnchecked(() -> { try (OutputStream out = new ChunkOutputStream(ctx, 1024)) { consumer.writeTo(out); } }); } writeFlushAndClose(ctx, LastHttpContent.EMPTY_LAST_CONTENT, isKeepAlive(rawRequest)); } } private class FileBody implements Body { private final File file; private FileBody(File file) { this.file = file; } @Override public void apply(HttpRequest rawRequest, WebsocketHandler websocketHandler, Request request, ChannelHandlerContext ctx) { HttpResponse response = new DefaultHttpResponse(HTTP_1_1, status); response.headers().add(headers); try { RandomAccessFile raf = new RandomAccessFile(file, "r"); long length = raf.length(); setContentLength(response, length); setKeepAlive(response, isKeepAlive(rawRequest)); // setting encoding to 'identity' so CustomCompressor will be skip compression when using FileRegion if (!request.isSecure()) { response.headers().set(Names.CONTENT_ENCODING, Values.IDENTITY); } ctx.write(response); if (!request.method().equals(Method.HEAD)) { if (request.isSecure()) { ctx.write(new HttpChunkedInput(new ChunkedFile(raf, 0, length, 8192)), ctx.newProgressivePromise()); } else { ctx.write(new DefaultFileRegion(raf.getChannel(), 0, length), ctx.newProgressivePromise()); } } writeFlushAndClose(ctx, LastHttpContent.EMPTY_LAST_CONTENT, isKeepAlive(rawRequest)); } catch (IOException e) { Titanite.LOG.error("error writing file to response", e); internalServerError().apply(rawRequest, websocketHandler, isKeepAlive(rawRequest), request, ctx); } } } }