net.oneandone.jasmin.main.Servlet.java Source code

Java tutorial

Introduction

Here is the source code for net.oneandone.jasmin.main.Servlet.java

Source

/**
 * Copyright 1&1 Internet AG, https://github.com/1and1/
 *
 * 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 net.oneandone.jasmin.main;

import net.oneandone.jasmin.model.Engine;
import net.oneandone.jasmin.model.File;
import net.oneandone.jasmin.model.Module;
import net.oneandone.jasmin.model.Resolver;
import net.oneandone.jasmin.model.Source;
import net.oneandone.sushi.fs.Node;
import net.oneandone.sushi.fs.World;
import net.oneandone.sushi.fs.file.FileNode;
import net.oneandone.sushi.fs.webdav.WebdavFilesystem;
import net.oneandone.sushi.fs.webdav.WebdavNode;
import net.oneandone.sushi.util.Strings;
import org.json.JSONException;
import org.json.JSONWriter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.Writer;
import java.net.InetAddress;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.UnknownHostException;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.List;

public class Servlet extends HttpServlet {
    public static final Logger LOG = LoggerFactory.getLogger(Servlet.class);

    private static final String HOSTNAME = getHostname();

    private static String getHostname() {
        try {
            return InetAddress.getLocalHost().getHostName();
        } catch (UnknownHostException e) {
            LOG.error("unknown hostname", e);
            return "unknown";
        }
    }

    // re-create engine if one of these files was changed; null if life resolving is off
    private List<Node> reloadFiles;
    private long loaded;
    private long otherVmStartupDate;

    private FileNode docroot;
    private Application application;

    // lazy init, because I need a URL first:
    private Engine engine;

    public Servlet() {
        // NOT longer than 10 years because the date format has only 2 digits for the year.
        this.otherVmStartupDate = VM_STARTUP_DATE.getTime() - TEN_YEARS;
    }

    /** creates configuration. */
    @Override
    public void init(ServletConfig config) throws ServletException {
        World world;
        String str;

        try {
            world = new World();
            configure(world, "http");
            configure(world, "https");
            str = config.getInitParameter("docroot");
            docroot = Application.file(world, str != null ? str : config.getServletContext().getRealPath(""));
            docroot.checkDirectory();
            LOG.info("home: " + world.getHome());
            application = Application.load(world, config, docroot);
            LOG.info("docroot: " + docroot);
        } catch (RuntimeException | Error e) {
            error(null, "init", e);
            throw e;
        } catch (Exception e) {
            error(null, "init", e);
            throw new ServletException(e);
        } catch (Throwable e) {
            error(null, "init", e);
            throw new RuntimeException("unexpected throwable", e);
        }
    }

    private static final int HTTP_TIMEOUT = 10 * 1000;

    private static void configure(World world, String scheme) {
        WebdavFilesystem webdav;

        webdav = world.getFilesystem(scheme, WebdavFilesystem.class);
        webdav.setDefaultConnectionTimeout(HTTP_TIMEOUT);
        webdav.setDefaultReadTimeout(HTTP_TIMEOUT);
    }

    /** Creates engine from configuration and resolve. Sychronized ensures we initialize only once. */
    private synchronized void lazyInit(HttpServletRequest request) throws IOException {
        List<File> files;
        URL url;
        long lastModified;
        long now;
        Resolver resolver;
        Node localhost;
        FileNode file;
        Object[] tmp;

        resolver = application.resolver;
        if (engine != null && resolver.isLife()) {
            for (Node node : reloadFiles) {
                lastModified = node.getLastModified();
                if (lastModified > loaded) {
                    now = System.currentTimeMillis();
                    if (lastModified > now) {
                        // fail to avoid repeated re-init
                        throw new IOException(node.getURI() + " has lastModifiedDate in the future: "
                                + new Date(lastModified) + "(now: " + new Date(now) + ")");
                    }
                    LOG.info("reloading jasmin for application '" + application.getContextPath()
                            + "' - changed file: " + node);
                    engine = null;
                    resolver.reset();
                }
            }
        }
        if (engine == null) {
            url = new URL(request.getRequestURL().toString());
            try {
                localhost = resolver.getWorld()
                        .node(new URI(url.getProtocol(), null, url.getHost(), url.getPort(), "", null, null));
            } catch (URISyntaxException e) {
                throw new IllegalStateException(e);
            }
            tmp = application.createEngine(docroot, localhost);
            engine = (Engine) tmp[0];
            LOG.info("started engine, initial url=" + url);
            for (Module module : engine.repository.modules()) {
                files = module.files();
                if (files.size() > 2) {
                    LOG.warn("deprecated: module '" + module.getName() + "' contains more than 2 file: " + files);
                }
                for (File f : files) {
                    if (f.getNormal() instanceof WebdavNode) {
                        LOG.warn("deprecated: module '" + module.getName() + "' uses base LOCALHOST: "
                                + f.getNormal().getURI());
                    }
                }
            }
            if (resolver.isLife()) {
                reloadFiles = (List<Node>) tmp[1];
                file = resolver.getLiveXml();
                if (file != null) {
                    reloadFiles.add(file);
                }
                LOG.info("reload if one of these " + reloadFiles.size() + " files is modified: ");
                for (Node node : reloadFiles) {
                    LOG.info("  " + node.getURI());
                }
                loaded = System.currentTimeMillis();
            }
            if (engine == null) {
                throw new IllegalStateException();
            }
        }
    }

    //--

    /**
     * Called by the servlet engine to process get requests:
     * a) to set the Last-Modified header in the response
     * b) to check if 304 can be redurned if the "if-modified-since" request header is present
     * @return -1 for when unknown
     */
    @Override
    public long getLastModified(HttpServletRequest request) {
        String path;
        int idx;
        long result;

        result = -1;
        try {
            path = request.getPathInfo();
            if (path != null && path.startsWith("/get/")) {
                lazyInit(request);
                path = path.substring(5);
                idx = path.indexOf('/');
                if (idx != -1) {
                    path = path.substring(idx + 1);
                    result = engine.getLastModified(path);
                }
            }
        } catch (IOException e) {
            error(request, "getLastModified", e);
            // fall-through
        } catch (RuntimeException | Error e) {
            error(request, "getLastModified", e);
            throw e;
        }
        LOG.debug("getLastModified(" + request.getPathInfo() + ") -> " + result);
        return result;
    }

    @Override
    public void doGet(HttpServletRequest request, HttpServletResponse response)
            throws IOException, ServletException {
        try {
            doGetUnchecked(request, response);
        } catch (IOException e) {
            // I can't compile against this class because the servlet api does not officially
            // report this situation ...
            // See http://tomcat.apache.org/tomcat-5.5-doc/catalina/docs/api/org/apache/catalina/connector/ClientAbortException.html
            if (e.getClass().getName().equals("org.apache.catalina.connector.ClientAbortException")) {
                // this is not an error: the client browser closed the response stream, e.g. because
                // the user already left the page
                LOG.info("aborted by client", e);
            } else {
                error(request, "get", e);
            }
            throw e;
        } catch (RuntimeException | Error e) {
            error(request, "get", e);
            throw e;
        }
    }

    private static final String MODULE_PREFIX = "/admin/module/";

    private void doGetUnchecked(HttpServletRequest request, HttpServletResponse response) throws IOException {
        String path;

        path = request.getPathInfo();
        if (path == null) {
            response.sendRedirect(request.getContextPath() + request.getServletPath() + "/");
            return;
        }
        lazyInit(request);
        LOG.debug("get " + path);
        if (path.startsWith("/get/")) {
            get(request, response, path.substring(5));
            return;
        }
        if (path.equals("/admin/")) {
            main(response);
            return;
        }
        if (path.equals("/admin/repository")) {
            repository(request, response);
            return;
        }
        if (path.equals("/admin/hashCache")) {
            hashCache(response);
            return;
        }
        if (path.equals("/admin/contentCache")) {
            contentCache(response);
            return;
        }
        if (path.startsWith(MODULE_PREFIX)) {
            module(request, response, path.substring(MODULE_PREFIX.length()));
            return;
        }
        if (path.equals("/admin/reload")) {
            reload(response);
            return;
        }
        if (path.equals("/admin/check")) {
            fileCheck(response);
            return;
        }
        notFound(request, response);
    }

    private static final long FIVE_MINUTES = 1000L * 60 * 5;
    private static final long SEVEN_DAYS = 1000L * 3600 * 24 * 7;
    private static final long TEN_YEARS = 1000L * 3600 * 24 * 365 * 10;

    private void get(HttpServletRequest request, HttpServletResponse response, String path) throws IOException {
        String version;
        boolean expire;
        int idx;
        long started;
        long duration;
        int bytes;
        boolean gzip;
        long date;

        idx = path.indexOf('/');
        if (idx == -1) {
            notFound(request, response);
            return;
        }
        started = System.currentTimeMillis();
        version = path.substring(0, idx);
        expire = !"no-expires".equals(version);
        if (expire && !VM_STARTUP_STR.equals(version)) {
            try {
                synchronized (FMT) {
                    date = FMT.parse(version).getTime();
                }
            } catch (ParseException e) {
                notFound(request, response);
                return;
            }
            if (sameTime(VM_STARTUP, date) || sameTime(otherVmStartupDate, date)) {
                // ok
            } else if (date > otherVmStartupDate) {
                otherVmStartupDate = date;
            } else {
                // usually, otherVmStartupDate is smaller, but after switching back, VM_STARTUP will be smaller
                if (Math.min(otherVmStartupDate, VM_STARTUP) - date > SEVEN_DAYS) {
                    gone(request, response);
                    return;
                }
            }
        }
        path = path.substring(idx + 1);
        if (application.resolver.isLife()) {
            // unknown headers are ok: see http://tools.ietf.org/html/rfc2616#section-7.1
            response.addHeader("Hi", "Sie werden bedient von Jasmin, vielen Dank fuer ihren Request!");
        }
        checkCharset(request.getHeader("Accept-Charset"));
        if (expire && application.expires != null) {
            response.setDateHeader("Expires", started + 1000L * application.expires);
            response.addHeader("Cache-Control", "max-age=" + application.expires);
        }
        gzip = canGzip(request);
        bytes = engine.request(path, response, gzip);
        duration = System.currentTimeMillis() - started;
        LOG.info(path + "|" + bytes + "|" + duration + "|" + gzip + "|" + referer(request));
    }

    private static boolean sameTime(long left, long right) {
        long diff;

        diff = left - right;
        if (diff < 0) {
            diff = -diff;
        }
        return diff < FIVE_MINUTES;
    }

    private static boolean canGzip(HttpServletRequest request) {
        String accepted;

        accepted = request.getHeader("Accept-Encoding");
        return accepted != null && contains(accepted, "gzip");
    }

    // see http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html
    public static void checkCharset(String accepts) throws IOException {
        if (accepts == null) {
            // ie7 does not send this header
            return;
        }
        // I've seen both "utf-8" and "UTF-8" -> test case-insensitive
        if (contains(accepts.toLowerCase(), Engine.ENCODING)) {
            return;
        }
        if (contains(accepts, "*")) {
            return;
        }
        throw new IOException(Engine.ENCODING + " encoding is not accepted: " + accepts);
    }

    // see http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html
    public static boolean contains(String list, String keyword) {
        int idx;
        int colon;
        String quality;

        idx = list.indexOf(keyword);
        if (idx == -1) {
            return false;
        }
        idx += keyword.length();
        colon = list.indexOf(",", idx);
        if (colon == -1) {
            colon = list.length();
        }
        quality = list.substring(idx, colon);
        idx = quality.indexOf('=');
        if (idx == -1) {
            return true;
        }
        return !"0".equals(quality.substring(idx + 1).trim());
    }

    private void main(HttpServletResponse response) throws IOException {
        html(response, "<p>Jasmin Servlet " + getVersion() + "</p>", "<p>Hostname: " + HOSTNAME + "</p>",
                "<p>Docroot: " + docroot.getAbsolute() + "</p>", "<p>VM Startup: " + VM_STARTUP_STR + "</p>",
                "<p>Other VM Startup: " + FMT.format(otherVmStartupDate) + "</p>",
                "<p>Loaded: " + new Date(loaded) + "</p>",
                "<p>HashCache: " + engine.hashCache.getMaxSize() + "</p>",
                "<p>ContentCache: " + engine.contentCache.getMaxSize() + "</p>",
                "<p>Requested Bytes: " + engine.requestedBytes.get() + "</p>",
                "<p>Computed Bytes: " + engine.computedBytes() + "</p>",
                "<p>Removed Bytes: " + engine.removedBytes() + "</p>", "<p>Load: " + engine.load() + "</p>",
                application.resolver.isLife() ? "<a href='reload'>Reload Files</a>" : "(no reload)",
                "<a href='repository'>Repository</a>", "<a href='hashCache'>Hash Cache</a>",
                "<a href='contentCache'>Content Cache</a>", "<a href='check'>File Check</a>");
    }

    private String getVersion() {
        return getClass().getPackage().getImplementationVersion();
    }

    private void repository(HttpServletRequest request, HttpServletResponse response) throws IOException {
        JSONWriter dest;
        List<Module> modules;

        response.setContentType("application/json");
        try (Writer writer = response.getWriter()) {
            dest = new JSONWriter(writer);
            dest.array();
            modules = new ArrayList<Module>(engine.repository.modules());
            Collections.sort(modules, new Comparator<Module>() {
                @Override
                public int compare(Module left, Module right) {
                    return left.getName().compareTo(right.getName());
                }
            });
            for (Module module : modules) {
                dest.object();
                dest.key("name");
                dest.value(module.getName());
                dest.key("details");
                dest.value(Strings.removeRight(request.getRequestURL().toString(), "/repository") + "/module/"
                        + module.getName());
                dest.endObject();
            }
            dest.endArray();
        } catch (JSONException e) {
            throw new IllegalStateException(e);
        }
    }

    private void hashCache(HttpServletResponse response) throws IOException {
        text(response, engine.hashCache.toString());
    }

    private void contentCache(HttpServletResponse response) throws IOException {
        text(response, engine.contentCache.toString());
    }

    private void module(HttpServletRequest request, HttpServletResponse response, String name) throws IOException {
        JSONWriter dest;
        Module module;
        Source source;

        module = engine.repository.lookup(name);
        source = module.getSource();
        if (module == null) {
            notFound(request, response);
            return;
        }
        response.setContentType("application/json");
        try (Writer writer = response.getWriter()) {
            dest = new JSONWriter(writer);
            dest.object();
            dest.key("name");
            dest.value(module.getName());
            dest.key("files");
            dest.array();
            for (File file : module.files()) {
                dest.object();
                dest.key("type");
                dest.value(file.getType());
                dest.key("normal");
                dest.value(file.getNormal().getURI());
                if (file.getMinimized() != null) {
                    dest.key("minimized");
                    dest.value(file.getMinimized().getURI());
                }
                dest.key("variant");
                dest.value(file.getVariant());
                dest.endObject();
            }
            dest.endArray();
            dest.key("dependencies");
            dest.array();
            for (Module dependency : module.dependencies()) {
                dest.value(dependency.getName());
            }
            dest.endArray();
            dest.key("source");
            dest.object();
            dest.key("artifactId");
            dest.value(source.artifactId);
            dest.key("groupId");
            dest.value(source.groupId);
            dest.key("version");
            dest.value(source.version);
            dest.key("scm");
            dest.value(source.scm);
            dest.endObject();
            dest.endObject();
        } catch (JSONException e) {
            throw new IllegalStateException(e);
        }
    }

    private void reload(HttpServletResponse response) throws IOException {
        String[] lines;

        lines = new String[reloadFiles.size()];
        for (int i = 0; i < lines.length; i++) {
            lines[i] = reloadFiles.get(i).getURI().toString();
        }
        text(response, lines);
    }

    private void fileCheck(HttpServletResponse response) throws IOException {
        FileCheck check;

        check = new FileCheck();
        check.minimize(true, engine.repository, application.resolver.getWorld());
        text(response, check.toString());
    }

    private void text(HttpServletResponse response, String... lines) throws IOException {
        response.setContentType("text/plain");
        try (Writer writer = response.getWriter()) {
            for (String line : lines) {
                writer.write(line);
                writer.write('\n');
            }
        }
    }

    private void html(HttpServletResponse response, String... lines) throws IOException {
        response.setContentType("text/html");
        try (Writer writer = response.getWriter()) {
            writer.write("<html><header></header><body>\n");
            for (String line : lines) {
                writer.write(line);
                writer.write('\n');
            }
            writer.write("</body>");
        }
    }

    private void notFound(HttpServletRequest request, HttpServletResponse response) throws IOException {
        LOG.warn("not found: " + request.getPathInfo());
        response.sendError(HttpServletResponse.SC_NOT_FOUND);
    }

    private void gone(HttpServletRequest request, HttpServletResponse response) throws IOException {
        LOG.warn("gone: " + request.getPathInfo());
        response.sendError(HttpServletResponse.SC_GONE);
    }

    //--

    /** @param request may be null */
    private void error(HttpServletRequest request, String method, Throwable throwable) {
        StringBuilder message;

        message = new StringBuilder();
        message.append(method).append(":").append(throwable.getMessage());
        if (request != null) {
            message.append('(');
            message.append("referer=").append(referer(request));
            message.append(",pathinfo=").append(pathInfo(request));
        }
        LOG.error(message.toString(), throwable);
    }

    private static String pathInfo(HttpServletRequest request) {
        if (request == null) {
            return null;
        }
        return request.getPathInfo();
    }

    private static String referer(HttpServletRequest request) {
        if (request == null) {
            return null;
        }
        return request.getHeader("Referer");
    }

    //-- XSLT functions

    public static final SimpleDateFormat FMT = new SimpleDateFormat("yyMMdd-HHmm");
    public static final Date VM_STARTUP_DATE = new Date();
    public static final long VM_STARTUP = VM_STARTUP_DATE.getTime();
    public static final String VM_STARTUP_STR = FMT.format(VM_STARTUP_DATE);

    public static String getVmStartup() {
        return VM_STARTUP_STR;
    }
}