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

org.webbitserver.handler.AbstractResourceHandler Maven / Gradle / Ivy

There is a newer version: 0.4.15
Show newest version
package org.webbitserver.handler;

import org.webbitserver.HttpControl;
import org.webbitserver.HttpHandler;
import org.webbitserver.HttpRequest;
import org.webbitserver.HttpResponse;

import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.Executor;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public abstract class AbstractResourceHandler implements HttpHandler {
    static {
        // This is not an exhaustive list, just the most common types. Call registerMimeType() to add more.
        Map mimeTypes = new HashMap();
        mimeTypes.put("txt", "text/plain");
        mimeTypes.put("css", "text/css");
        mimeTypes.put("csv", "text/csv");
        mimeTypes.put("htm", "text/html");
        mimeTypes.put("html", "text/html");
        mimeTypes.put("xml", "text/xml");
        mimeTypes.put("js",
                      "text/javascript"); // Technically it should be application/javascript (RFC 4329), but IE8 struggles with that
        mimeTypes.put("xhtml", "application/xhtml+xml");
        mimeTypes.put("json", "application/json");
        mimeTypes.put("pdf", "application/pdf");
        mimeTypes.put("zip", "application/zip");
        mimeTypes.put("tar", "application/x-tar");
        mimeTypes.put("gif", "image/gif");
        mimeTypes.put("jpeg", "image/jpeg");
        mimeTypes.put("jpg", "image/jpeg");
        mimeTypes.put("tiff", "image/tiff");
        mimeTypes.put("tif", "image/tiff");
        mimeTypes.put("png", "image/png");
        mimeTypes.put("svg", "image/svg+xml");
        mimeTypes.put("ico", "image/vnd.microsoft.icon");
        DEFAULT_MIME_TYPES = Collections.unmodifiableMap(mimeTypes);
    }

    private static final Pattern SINGLE_BYTE_RANGE = Pattern.compile("bytes=(\\d+)?-(\\d+)?");
    public static final Map DEFAULT_MIME_TYPES;
    protected static final String DEFAULT_WELCOME_FILE_NAME = "index.html";
    protected final Executor ioThread;
    protected final Map mimeTypes;
    protected String welcomeFileName;

    public AbstractResourceHandler(Executor ioThread) {
        this.ioThread = ioThread;
        this.mimeTypes = new HashMap(DEFAULT_MIME_TYPES);
        this.welcomeFileName = DEFAULT_WELCOME_FILE_NAME;
    }

    public AbstractResourceHandler addMimeType(String extension, String mimeType) {
        mimeTypes.put(extension, mimeType);
        return this;
    }

    public AbstractResourceHandler welcomeFile(String welcomeFile) {
        this.welcomeFileName = welcomeFile;
        return this;
    }

    @Override
    public void handleHttpRequest(final HttpRequest request, final HttpResponse response, final HttpControl control)
            throws Exception
    {
        // Switch from web thead to IO thread, so we don't block web server when we access the filesystem.
        ioThread.execute(createIOWorker(request, response, control));
    }

    protected void serve(final String mimeType,
                         final ByteBuffer contents,
                         HttpControl control,
                         final HttpResponse response,
                         final HttpRequest request)
    {
        // Switch back from IO thread to web thread.
        control.execute(new Runnable() {
            @Override
            public void run() {
                // TODO: Check bytes read match expected encoding of mime-type
                response.header("Content-Type", mimeType);

                if (maybeServeRange(request, contents, response)) {
                    return;
                }


                // TODO: Don't read all into memory, instead use zero-copy.
                response.header("Content-Length", contents.remaining())
                        .content(contents)
                        .end();
            }
        });
    }

    private boolean maybeServeRange(HttpRequest request, ByteBuffer contents, HttpResponse response) {
        String range = request.header("Range");
        if (null == range) {
            return false;
        }

        Matcher matcher = SINGLE_BYTE_RANGE.matcher(range);
        if (!matcher.matches()) {
            return false;
        }

        String startString = matcher.group(1);
        String endString = matcher.group(2);

        if (null != startString && null != endString) {
            int start = Integer.parseInt(startString);
            int end = Integer.parseInt(endString);
            if (start <= end) {
                serveRange(start,
                           Math.min(contents.remaining() - 1, end),
                           contents,
                           response);
                return true;
            }
        } else if (null != startString) {
            serveRange(Integer.parseInt(startString),
                       contents.remaining() - 1,
                       contents,
                       response);
            return true;
        } else if (null != endString) {
            int end = Integer.parseInt(endString);
            serveRange(contents.remaining() - end,
                       contents.remaining() - 1,
                       contents,
                       response);
            return true;
        }

        return false;
    }

    protected void serveRange(int start, int end, ByteBuffer contents, HttpResponse response) {
        if (start > contents.remaining()) {
            response.status(416).header("Content-Range", "bytes */" + contents.remaining()).end();
            return;
        }


        response.status(206)
                .header("Content-Length", end - start + 1) // since its inclusive
                .header("Content-Range",
                        "bytes " + start + "-" + end + "/" + contents.remaining());

        contents.limit(contents.position() + end + 1)
                .position(contents.position() + start);
        response.content(contents).end();
    }

    protected abstract StaticFileHandler.IOWorker createIOWorker(HttpRequest request,
                                                                 HttpResponse response,
                                                                 HttpControl control);

    /**
     * All IO is performed by this worker on a separate thread, so we never block the HttpHandler.
     */
    protected abstract class IOWorker implements Runnable {

        protected String path;
        private final HttpRequest request;
        protected final HttpResponse response;
        protected final HttpControl control;

        protected IOWorker(String path, HttpRequest request, HttpResponse response, HttpControl control) {
            this.path = path;
            this.request = request;
            this.response = response;
            this.control = control;
        }

        protected void notFound() {
            // Switch back from IO thread to web thread.
            control.execute(new Runnable() {
                @Override
                public void run() {
                    control.nextHandler();
                }
            });
        }

        protected void error(final IOException exception) {
            // Switch back from IO thread to web thread.
            control.execute(new Runnable() {
                @Override
                public void run() {
                    response.error(exception);
                }
            });
        }

        @Override
        public void run() {
            path = withoutTrailingSlashOrQuery(path);

            // TODO: Cache
            // TODO: If serving directory and trailing slash omitted, perform redirect
            try {
                ByteBuffer content = null;
                if (!exists()) {
                    notFound();
                } else if ((content = fileBytes()) != null) {
                    serve(guessMimeType(path), content, control, response, request);
                } else {
                    if ((content = welcomeBytes()) != null) {
                        serve(guessMimeType(welcomeFileName), content, control, response, request);
                    } else {
                        notFound();
                    }
                }
            } catch (IOException e) {
                error(e);
            }
        }

        protected abstract boolean exists() throws IOException;

        protected abstract ByteBuffer fileBytes() throws IOException;

        protected abstract ByteBuffer welcomeBytes() throws IOException;

        protected ByteBuffer read(int length, InputStream in) throws IOException {
            byte[] data = new byte[length];
            try {
                int read = 0;
                while (read < length) {
                    int more = in.read(data, read, data.length - read);
                    if (more == -1) {
                        break;
                    } else {
                        read += more;
                    }
                }
            } finally {
                in.close();
            }
            return ByteBuffer.wrap(data);
        }

        // TODO: Don't respond with a mime type that violates the request's Accept header
        private String guessMimeType(String path) {
            int lastDot = path.lastIndexOf('.');
            if (lastDot == -1) {
                return null;
            }
            String extension = path.substring(lastDot + 1).toLowerCase();
            String mimeType = mimeTypes.get(extension);
            if (mimeType == null) {
                return null;
            }
            if (mimeType.startsWith("text/") && response.charset() != null) {
                mimeType += "; charset=" + response.charset().name();
            }
            return mimeType;
        }

        protected String withoutTrailingSlashOrQuery(String path) {
            int queryStart = path.indexOf('?');
            if (queryStart > -1) {
                path = path.substring(0, queryStart);
            }
            if (path.endsWith("/")) {
                path = path.substring(0, path.length() - 1);
            }
            return path;
        }

    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy