Java tutorial
/* * Leaves Framework * Copyright (C) 2015 Hugo Miard * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ package com.hmiard.leaves.webserver.Http; import com.hmiard.leaves.webserver.Leaves; import com.hmiard.leaves.webserver.Utils.FileUtils; import org.joda.time.DateTime; import java.io.*; import java.util.*; /** * Basic response that is returned by Leaves. */ public class LeavesResponse { public static final int BUFFSIZE = 16 * 1024; /** * MIME type of the content, e.g "text/html". */ private String mime; /** * HTTP status, e.g 200 "OK", 404 "NOT FOUND", etc... */ private Status status; /** * Data that will be printed in the response body. May be null. */ private InputStream data; /** * The response headers. * Use addHeader() to push new headers. */ private HashMap<String, String> header = new HashMap<>(); /** * The method from the request behind the response. */ private GenericSession.Method requestMethod; /** * The response body will be chunked. */ private boolean chunk; /** * Default constructor. * * @param content String */ public LeavesResponse(String content) { this(Status.OK, CommonMimeTypes.HTML, content); } public LeavesResponse(Status status) { this(status, CommonMimeTypes.PLAINTEXT, ""); } /** * Reading the content from an input stream. * * @param status Status * @param mimeType String * @param data InputStream */ public LeavesResponse(Status status, String mimeType, InputStream data) { this.status = status; this.mime = mimeType; this.data = data; } /** * Trying to create an input stream from and UTF-8 encoded string. * * @param status Status * @param mimeType String * @param content String */ public LeavesResponse(Status status, String mimeType, String content) { this.status = status; this.mime = mimeType; try { this.data = (content != null && !content.equals("")) ? new ByteArrayInputStream(content.getBytes("UTF-8")) : null; } catch (UnsupportedEncodingException e) { Leaves.say("Unsupported encoding for the response content ! It must be encoded in UTF-8."); this.data = new ByteArrayInputStream(content.getBytes()); } } /** * Adding an header to the response body. * * @param name String * @param value String */ public void addHeader(String name, String value) { this.header.put(name, value); } public void deleteHeader(String name) { this.header.remove(name); } public String getHeader(String name) { return this.header.get(name); } /** * Sending the response body. * * @param socket OutputStream * @throws Error */ protected void send(OutputStream socket) throws Error { DateTime date = new DateTime(); try { if (status == null) throw new Error("Response.send() : the status can't be null !"); PrintWriter pw = new PrintWriter(socket); pw.print("HTTP/1.1 " + status.getDescription() + " \r\n"); if (mime != null) pw.print("Content-Type: " + mime + "\r\n"); if (header == null || header.get("Date") == null) pw.print("Date: " + date.toString() + "\r\n"); if (header != null) for (String key : header.keySet()) pw.print(key + ": " + header.get(key) + "\r\n"); sendConnection(pw, header); if (requestMethod != GenericSession.Method.HEAD && chunk) sendAsChunked(socket, pw); else { int pending = (data != null) ? data.available() : 0; sendContentLength(pw, header, pending); pw.print("\r\n"); pw.flush(); sendAsFixedLength(socket, pending); } socket.flush(); FileUtils.close(data); } catch (IOException ioe) { Leaves.say("Couldn't write the response body"); } } /** * Adding specific headers to the response body. * * @param pw PrintWriter * @param header HashMap * @param size int */ protected void sendContentLength(PrintWriter pw, HashMap<String, String> header, int size) { if (!headerAlreadySent(header, "content-length")) pw.print("Content-Length: " + size + "\r\n"); } protected void sendConnection(PrintWriter pw, HashMap<String, String> header) { if (!headerAlreadySent(header, "connection")) pw.print("Connection: keep-alive\r\n"); } /** * Checking if an header is already in the header map. * * @param header HashMap * @param name String * @return boolean */ private boolean headerAlreadySent(HashMap<String, String> header, String name) { for (String headerName : header.keySet()) if (headerName.equalsIgnoreCase(name)) return true; return false; } /** * Writing the chunked response body. * * @param outputStream OutputStream * @param pw PrintWriter * @throws IOException */ private void sendAsChunked(OutputStream outputStream, PrintWriter pw) throws IOException { pw.print("Transfer-Encoding: chunked\r\n"); pw.print("\r\n"); pw.flush(); byte[] buff = new byte[BUFFSIZE]; byte[] CRLF = "\r\n".getBytes(); int read; while ((read = data.read(buff)) > 0) { outputStream.write(String.format("%x\r\n", read).getBytes()); outputStream.write(buff, 0, read); outputStream.write(CRLF); } outputStream.write("0\r\n\r\n".getBytes()); } /** * Will send the response body with only the first BUFFSIZE (at max) bytes of data. * * @param outputStream OutputStream * @param pending int * @throws IOException */ private void sendAsFixedLength(OutputStream outputStream, int pending) throws IOException { if (requestMethod != GenericSession.Method.HEAD && data != null) { byte[] buff = new byte[BUFFSIZE]; while (pending > 0) { int read = data.read(buff, 0, ((pending > BUFFSIZE) ? BUFFSIZE : pending)); if (read <= 0) break; outputStream.write(buff, 0, read); pending -= read; } } } public void setAs301Redirection(String location) { this.status = Status.REDIRECT; this.addHeader("Location", location); } public String getMime() { return mime; } public void setMime(String mime) { this.mime = mime; } public boolean isChunk() { return chunk; } public void setChunk(boolean chunk) { this.chunk = chunk; } public Status getStatus() { return status; } public void setStatus(Status status) { this.status = status; } public InputStream getData() { return data; } public void setData(InputStream data) { this.data = data; } public GenericSession.Method getRequestMethod() { return requestMethod; } public void setRequestMethod(GenericSession.Method requestMethod) { this.requestMethod = requestMethod; } /** * Response HTTP status. */ public static enum Status { SWITCH_PROTOCOL(101, "Switching Protocols"), OK(200, "OK"), CREATED(201, "Created"), ACCEPTED(202, "Accepted"), NO_CONTENT(204, "No Content"), PARTIAL_CONTENT(206, "Partial Content"), REDIRECT(301, "Moved Permanently"), NOT_MODIFIED(304, "Not Modified"), BAD_REQUEST(400, "Bad Request"), UNAUTHORIZED(401, "Unauthorized"), FORBIDDEN(403, "Forbidden"), NOT_FOUND(404, "Not Found"), METHOD_NOT_ALLOWED(405, "Method Not Allowed"), RANGE_NOT_SATISFIABLE(416, "Requested Range Not Satisfiable"), INTERNAL_ERROR(500, "Internal Server Error"); private final int requestStatus; private final String description; Status(int requestStatus, String description) { this.requestStatus = requestStatus; this.description = description; } public int getRequestStatus() { return this.requestStatus; } public String getDescription() { return "" + this.requestStatus + " " + description; } } /** * Common MIME types. */ public static class CommonMimeTypes { public static final String PLAINTEXT = "text/plain"; public static final String HTML = "text/html"; public static final String CSS = "text/css"; public static final String JS = "application/javascript"; public static final String OGG = "application/ogg"; public static final String PDF = "application/pdf"; public static final String ZIP = "application/zip"; public static final String XML = "application/xml"; public static final String MPEG_AUDIO = "audio/mpeg"; public static final String MP3_CHROMIUM = "audio/mp3"; public static final String WAV = "audio/x-wav"; public static final String WAV_CHROMIUM = "audio/wav"; public static final String WMA = "audio/x-ms-wma"; public static final String GIF = "image/gif"; public static final String PNG = "image/png"; public static final String JPEG = "image/jpeg"; public static final String TIFF = "image/tiff"; public static final String ICO = "image/vnd.microsoft.icon"; public static final String MPEG_VIDEO = "video/mpeg"; public static final String MP4 = "video/mp4"; public static final String WMV = "video/x-ms-wmv"; public static final String AVI = "video/x-msvideo"; public static final String FLV = "video/x-flv"; /** * Returns the proper mime type to use after a file extension. * * @param ext String * @param isChromium boolean * @return the mime type */ public static String interpretExtension(String ext, boolean isChromium) { String mime; switch (ext) { case "html": mime = HTML; break; case "css": mime = CSS; break; case "js": mime = JS; break; case "ogg": mime = OGG; break; case "pdf": mime = PDF; break; case "zip": mime = ZIP; break; case "xml": mime = XML; break; case "mp3": mime = (isChromium) ? MP3_CHROMIUM : MPEG_AUDIO; break; case "wav": mime = (isChromium) ? WAV_CHROMIUM : WAV; break; case "wma": mime = WMA; break; case "gif": mime = GIF; break; case "png": mime = PNG; break; case "jpg": mime = JPEG; break; case "jpeg": mime = JPEG; break; case "tiff": mime = TIFF; break; case "ico": mime = ICO; break; case "mpg": mime = MPEG_VIDEO; break; case "mpeg": mime = MPEG_VIDEO; break; case "mp4": mime = MP4; break; case "wmv": mime = WMV; break; case "avi": mime = AVI; break; case "flv": mime = FLV; break; default: mime = PLAINTEXT; } return mime; } } /** * Response Exception. */ public static final class ResponseException extends Exception { private final Status status; public ResponseException(Status status, String message) { super(message); this.status = status; } public ResponseException(Status status, String message, Exception e) { super(message, e); this.status = status; } public Status getStatus() { return status; } } }