com.hmiard.leaves.webserver.Http.LeavesResponse.java Source code

Java tutorial

Introduction

Here is the source code for com.hmiard.leaves.webserver.Http.LeavesResponse.java

Source

/*
 * 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;
        }
    }
}