com.sonicle.webtop.core.app.servlet.ResourceRequest.java Source code

Java tutorial

Introduction

Here is the source code for com.sonicle.webtop.core.app.servlet.ResourceRequest.java

Source

/*
 * WebTop Services is a Web Application framework developed by Sonicle S.r.l.
 * Copyright (C) 2014 Sonicle S.r.l.
 *
 * This program is free software; you can redistribute it and/or modify it under
 * the terms of the GNU Affero General Public License version 3 as published by
 * the Free Software Foundation with the addition of the following permission
 * added to Section 15 as permitted in Section 7(a): FOR ANY PART OF THE COVERED
 * WORK IN WHICH THE COPYRIGHT IS OWNED BY SONICLE, SONICLE DISCLAIMS THE
 * WARRANTY OF NON INFRINGEMENT OF THIRD PARTY RIGHTS.
 *
 * 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 Affero General Public License
 * along with this program; if not, see http://www.gnu.org/licenses or write to
 * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
 * MA 02110-1301 USA.
 *
 * You can contact Sonicle S.r.l. at email address sonicle@sonicle.com
 *
 * The interactive user interfaces in modified source and object code versions
 * of this program must display Appropriate Legal Notices, as required under
 * Section 5 of the GNU Affero General Public License version 3.
 *
 * In accordance with Section 7(b) of the GNU Affero General Public License
 * version 3, these Appropriate Legal Notices must retain the display of the
 * Sonicle logo and Sonicle copyright notice. If the display of the logo is not
 * reasonably feasible for technical reasons, the Appropriate Legal Notices must
 * display the words "Copyright (C) 2014 Sonicle S.r.l.".
 */
package com.sonicle.webtop.core.app.servlet;

import com.sonicle.commons.LangUtils;
import com.sonicle.commons.PropertiesEx;
import com.sonicle.commons.RegexUtils;
import com.sonicle.commons.web.ServletUtils;
import com.sonicle.webtop.core.app.CoreManifest;
import com.sonicle.webtop.core.app.ServiceManager;
import com.sonicle.webtop.core.app.SessionContext;
import com.sonicle.webtop.core.app.WT;
import com.sonicle.webtop.core.app.WebTopApp;
import com.sonicle.webtop.core.app.WebTopManager;
import com.sonicle.webtop.core.app.WebTopSession;
import com.sonicle.webtop.core.io.Resource;
import com.sonicle.webtop.core.sdk.ServiceManifest;
import com.sonicle.webtop.core.sdk.WTRuntimeException;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.MalformedURLException;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.ArrayList;
import java.util.concurrent.ConcurrentHashMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;

/**
 *
 * @author malbinola
 */
public class ResourceRequest extends HttpServlet {
    public static final String URL = "/resources"; // Shiro.ini must reflect this URI!
    private static final Logger logger = WT.getLogger(ResourceRequest.class);
    protected static final int BUFFER_SIZE = 4 * 1024;
    private static final Pattern PATTERN_VIRTUAL_URL = Pattern
            .compile("^" + RegexUtils.MATCH_URL_SEPARATOR + RegexUtils.capture(RegexUtils.MATCH_JAVA_PACKAGE)
                    + RegexUtils.MATCH_URL_SEPARATOR + RegexUtils.capture(RegexUtils.MATCH_SW_VERSION)
                    + RegexUtils.MATCH_URL_SEPARATOR + RegexUtils.capture(RegexUtils.MATCH_ANY) + "$");
    private static final Pattern PATTERN_LAF_PATH = Pattern.compile("^laf\\/([\\w\\-\\.]+)\\/(.*)$");
    private static final Pattern PATTERN_LOCALE_FILE = Pattern.compile("^(Locale_(\\w*)).js$");
    private static final ConcurrentHashMap<String, Long> lastModifiedCache = new ConcurrentHashMap<>();

    @Override
    protected void doPut(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        lookup(request).respondGet(request, response);
    }

    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        lookup(request).respondGet(request, response);
    }

    @Override
    protected void doHead(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        try {
            lookup(request).respondHead(request, response);
        } catch (UnsupportedOperationException ex) {
            super.doHead(request, response);
        }
    }

    @Override
    protected long getLastModified(HttpServletRequest request) {
        return lookup(request).getLastModified();
        /*
        String pathInfo = request.getPathInfo();
        if(lastModifiedCache.containsKey(pathInfo)) {
           return lastModifiedCache.get(pathInfo);
        } else {
           return lookup(request).getLastModified();
        }
        */
    }

    protected LookupResult lookup(HttpServletRequest req) {
        LookupResult r = (LookupResult) req.getAttribute("lookupResult");
        if (r == null) {
            String pathInfo = req.getPathInfo();
            r = lookupNoCache(req, pathInfo);
            //if (r.cacheLastModified()) lastModifiedCache.put(pathInfo, r.getLastModified());
            req.setAttribute("lookupResult", r);
        }
        return r;
    }

    private String[] splitPath(String pathInfo) throws MalformedURLException {
        String[] tokens = StringUtils.split(pathInfo, "/", 2);
        if (tokens.length != 2)
            throw new MalformedURLException("URL does not esplicitate service ID");
        return tokens;
    }

    protected LookupResult lookupNoCache(HttpServletRequest req, String reqPath) {
        String subject = null, subjectPath = null, jsPath = null, path = null, targetPath = null;
        boolean isVirtualUrl = false, jsPathFound = false;
        URL targetUrl = null;

        try {
            WebTopApp wta = WebTopApp.get(req);

            // Builds a convenient URL for the servlet relative URL
            try {
                //String reqPath = req.getPathInfo();
                //logger.trace("Requested path [{}]", reqPath);

                Matcher matcher = PATTERN_VIRTUAL_URL.matcher(reqPath);
                if (matcher.matches()) {
                    // Matches URLs like: /{service.id}/{service.version}/{remaining.url.part}
                    // Eg. /com.sonicle.webtop.core/5.1.1/laf/default/service.css
                    //   {service.id} -> com.sonicle.webtop.core
                    //   {service.version} -> 5.1.1
                    //   {remaining.url.part} -> laf/default/service.css
                    isVirtualUrl = true;
                    subject = matcher.group(1);
                    path = matcher.group(3);

                    if (!wta.getServiceManager().hasService(subject)) {
                        return new Error(HttpServletResponse.SC_BAD_REQUEST, "Bad Request");
                    }
                    targetUrl = new URL("http://fake/client/" + subject + "/" + path);

                } else {
                    isVirtualUrl = false;
                    String[] urlParts = splitPath(reqPath);
                    subject = urlParts[0];
                    jsPath = wta.getServiceManager().getServiceJsPath(subject);
                    jsPathFound = (jsPath != null);
                    subjectPath = (jsPathFound) ? jsPath : urlParts[0];
                    path = urlParts[1];

                    targetUrl = new URL("http://fake/" + subjectPath + "/" + path);
                }
                targetPath = targetUrl.getPath();

                //logger.trace("Translated path [{}]", translPath);
                if (isForbidden(targetPath))
                    return new Error(HttpServletResponse.SC_FORBIDDEN, "Forbidden");

            } catch (MalformedURLException ex1) {
                return new Error(HttpServletResponse.SC_BAD_REQUEST, "Bad Request");
            }

            if (!isVirtualUrl && path.startsWith("images")) {
                // Addresses domain public images
                // URLs like "/{domainPublicName}/images/{relativePathToFile}"
                // Eg.   "/1bbc048f/images/login.png"
                //      "/1bbc048f/images/sub/login.png"
                WebTopManager wtMgr = wta.getWebTopManager();

                String domainId = wtMgr.publicNameToDomainId(subject);
                if (StringUtils.isBlank(domainId)) {
                    // We must support old-style URL using {domainInternetName}
                    // instead of {domainPublicName}
                    // Eg.   "/sonicle.com/images/login.png"
                    domainId = wtMgr.internetNameToDomain(subject);
                }
                if (StringUtils.isBlank(domainId)) {
                    return new Error(HttpServletResponse.SC_BAD_REQUEST, "Bad Request");
                }

                return lookupDomainImage(req, targetUrl, domainId);

            } else if (isVirtualUrl && subject.equals(CoreManifest.ID)
                    && path.equals("resources/images/login.png")) {
                // Addresses login image
                // URLs like "/{serviceId}/{serviceVersion}/resources/images/login.png"
                // Eg.   "/com.sonicle.webtop.core/5.0.0/images/login.png"
                return lookupLoginImage(req, targetUrl);

            } else if (isVirtualUrl && subject.equals(CoreManifest.ID) && path.equals("resources/license.html")) {
                // Addresses licence page
                // URLs like "/{serviceId}/{serviceVersion}/resources/license.html"
                return lookupLicense(req, targetUrl);

            } else if (!isVirtualUrl && path.startsWith("whatsnew/")) {
                return lookupWhatsnew(req, targetUrl, path, subject);
            } else {
                if (StringUtils.endsWith(targetPath, ".js")) {
                    String sessionId = ServletHelper.getSessionID(req);
                    if (StringUtils.startsWith(path, "resources/vendor")
                            || StringUtils.startsWith(path, "resources/libs")) {
                        // If targets lib folder, simply return requested file without handling debug versions
                        return lookupJs(req, targetUrl, false);

                    } else if (StringUtils.startsWith(path, "boot/")) {
                        return lookupJs(req, targetUrl, isJsDebug());

                    } else if (StringUtils.startsWith(FilenameUtils.getBaseName(path), "Locale")) {
                        return lookupLocaleJs(req, targetUrl, subject);

                    } else {
                        return lookupJs(req, targetUrl, isJsDebug());
                    }

                } else if (StringUtils.startsWith(path, "laf")) {
                    return lookupLAF(req, targetUrl, path, subject, subjectPath);

                } else {
                    return lookupDefault(req, isVirtualUrl ? ClientCaching.YES : ClientCaching.AUTO, targetUrl);
                }
            }

        } catch (ServletException ex) {
            return new Error(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, ex.getMessage());
        }
    }

    private boolean isJsDebug() {
        WebTopSession wts = SessionContext.getCurrent();
        return (wts != null) ? wts.isJsDebugEnabled() : false;
    }

    private URL getResURL(String name) {
        //logger.trace("Try getting resource [{}]", name);
        return this.getClass().getResource(name);
    }

    private LookupResult lookupDomainImage(HttpServletRequest request, URL targetUrl, String domainId) {
        String targetPath = targetUrl.getPath();
        URL fileUrl = null;

        try {
            WebTopApp wta = WebTopApp.get(request);
            String remainingPath = StringUtils.substringAfter(targetPath, "images/");
            if (StringUtils.isBlank(remainingPath))
                throw new NotFoundException();
            String imagesPath = wta.getImagesPath(domainId);
            File file = new File(imagesPath + remainingPath);
            if (!file.exists())
                throw new NotFoundException();
            fileUrl = file.toURI().toURL();

            Resource resFile = getFile(wta, fileUrl);
            return new StaticFile(fileUrl.toString(), getMimeType(targetPath), ClientCaching.NO, resFile);

        } catch (ForbiddenException ex) {
            return new Error(HttpServletResponse.SC_FORBIDDEN, "Forbidden");
        } catch (MalformedURLException | NotFoundException ex) {
            return new Error(HttpServletResponse.SC_NOT_FOUND, "Not Found");
        } catch (InternalServerException ex) {
            return new Error(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "Internal server error");
        } catch (ServletException ex) {
            return new Error(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, ex.getMessage());
        }
    }

    private LookupResult lookupLoginImage(HttpServletRequest request, URL targetUrl) {
        String targetPath = targetUrl.getPath();
        URL fileUrl = null;

        try {
            WebTopApp wta = WebTopApp.get(request);
            //String internetName = ServletUtils.getInternetName(request);
            String internetName = ServletUtils.getHost(request);
            String domainId = WT.findDomainIdByInternetName(internetName);
            if (!StringUtils.isBlank(domainId)) {
                String pathname = wta.getImagesPath(domainId) + "login.png";
                File file = new File(pathname);
                if (file.exists()) {
                    fileUrl = file.toURI().toURL();
                }
            }

            if (fileUrl == null) {
                fileUrl = getResURL(targetPath);
            }

            Resource resFile = getFile(wta, fileUrl);
            StaticFile sf = new StaticFile(fileUrl.toString(), getMimeType(targetPath), ClientCaching.NO, resFile);
            sf.cacheLastModified = false;
            return sf;

        } catch (ForbiddenException ex) {
            return new Error(HttpServletResponse.SC_FORBIDDEN, "Forbidden");
        } catch (MalformedURLException | NotFoundException ex) {
            return new Error(HttpServletResponse.SC_NOT_FOUND, "Not Found");
        } catch (InternalServerException ex) {
            return new Error(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "Internal server error");
        } catch (ServletException ex) {
            return new Error(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, ex.getMessage());
        }
    }

    private LookupResult lookupLicense(HttpServletRequest request, URL targetUrl) {
        String targetPath = targetUrl.getPath();
        URL fileUrl = null;

        try {
            WebTopApp wta = WebTopApp.get(request);
            WebTopManager wtMgr = wta.getWebTopManager();
            if (wtMgr != null) {
                String internetName = ServletUtils.getInternetName(request);
                String domainId = wtMgr.internetNameToDomain(internetName);
                if (!StringUtils.isBlank(domainId)) {
                    String pathname = wta.getHomePath(domainId) + "license.html";
                    File file = new File(pathname);
                    if (file.exists()) {
                        fileUrl = file.toURI().toURL();
                    }
                }
            }

            if (fileUrl == null) {
                fileUrl = getResURL(targetPath);
            }

            Resource resFile = getFile(wta, fileUrl);
            return new StaticFile(fileUrl.toString(), getMimeType(targetPath), ClientCaching.NO, resFile);

        } catch (MalformedURLException | ForbiddenException ex) {
            return new Error(HttpServletResponse.SC_FORBIDDEN, "Forbidden");
        } catch (NotFoundException ex) {
            return new Error(HttpServletResponse.SC_NOT_FOUND, "Not Found");
        } catch (InternalServerException ex) {
            return new Error(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "Internal server error");
        } catch (ServletException ex) {
            return new Error(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, ex.getMessage());
        }
    }

    private LookupResult lookupJs(HttpServletRequest request, URL targetUrl, boolean debugVersion) {
        String targetPath = targetUrl.getPath();
        URL fileUrl = null;

        try {
            if (debugVersion) {
                String dpath = targetPath.substring(0, targetPath.length() - 3) + "-debug.js";
                fileUrl = getResURL(dpath);
                if (fileUrl != null)
                    targetPath = dpath;
            }
            if (fileUrl == null) {
                fileUrl = getResURL(targetPath);
            }

            Resource resFile = getFile(WebTopApp.get(request), fileUrl);
            return new StaticFile(fileUrl.toString(), getMimeType(targetPath), ClientCaching.YES, resFile);

        } catch (ForbiddenException ex) {
            return new Error(HttpServletResponse.SC_FORBIDDEN, "Forbidden");
        } catch (NotFoundException ex) {
            return new Error(HttpServletResponse.SC_NOT_FOUND, "Not Found");
        } catch (InternalServerException ex) {
            return new Error(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "Internal server error");
        } catch (ServletException ex) {
            return new Error(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, ex.getMessage());
        }
    }

    private LookupResult lookupLocaleJs(HttpServletRequest request, URL targetUrl, String serviceId) {
        String targetPath = targetUrl.getPath();
        URL fileUrl = null;

        try {
            WebTopApp wta = WebTopApp.get(request);
            String fileName = FilenameUtils.getName(targetPath);
            String baseTargetPath = StringUtils.substringBefore(targetPath, fileName);
            Matcher matcher = PATTERN_LOCALE_FILE.matcher(fileName);
            if (!matcher.matches())
                throw new InternalServerException();
            String nameBase = matcher.group(1);
            String nameLoc = matcher.group(2);
            String[] tokens = StringUtils.split(nameLoc, "_", 2);

            // Try to get the properties file that match the requested locale...
            // If not found, look for the basic english locale (en)
            String[] suffixes = null;
            if (tokens.length == 2) {
                suffixes = new String[] { nameLoc, tokens[0], "en" };
            } else {
                suffixes = new String[] { nameLoc, "en" };
            }
            for (String suffix : suffixes) {
                fileUrl = getResURL(baseTargetPath + "locale_" + suffix + ".properties");
                if (fileUrl != null)
                    break;
            }
            if (fileUrl == null)
                throw new NotFoundException();

            // Defines specific params
            ServiceManager svcm = wta.getServiceManager();
            ServiceManifest manifest = svcm.getManifest(serviceId);
            String clazz = manifest.getJsPackageName() + "." + nameBase;
            String override = manifest.getPrivateServiceJsClassName(true);

            //logger.trace("Class: {} - Override: {}", clazz, override);
            Resource resFile = getFile(wta, fileUrl);
            return new LocaleJsFile(clazz, override, fileUrl.toString(), ClientCaching.YES, resFile);

        } catch (ForbiddenException ex) {
            return new Error(HttpServletResponse.SC_FORBIDDEN, "Forbidden");
        } catch (NotFoundException ex) {
            return new Error(HttpServletResponse.SC_NOT_FOUND, "Not Found");
        } catch (InternalServerException ex) {
            return new Error(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "Internal server error");
        } catch (ServletException ex) {
            return new Error(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, ex.getMessage());
        }
    }

    private LookupResult lookupLAF(HttpServletRequest request, URL targetUrl, String path, String serviceId,
            String subjectPath) {
        URL fileUrl = null;

        try {
            String baseTargetPath = StringUtils.substringBefore(targetUrl.getPath(), path);
            Matcher lafm = PATTERN_LAF_PATH.matcher(path);
            if (!lafm.matches())
                return new Error(HttpServletResponse.SC_BAD_REQUEST, "Bad Request");
            String pathLaf = lafm.group(1);
            String remainingPath = lafm.group(2);

            // Try to get resource in folder related to the requested look&feel...
            // If not found, look for the default one (default)
            String[] lafs = new String[] { pathLaf, "default" };
            for (String laf : lafs) {
                fileUrl = getResURL(baseTargetPath + "laf/" + laf + "/" + remainingPath);
                if (fileUrl != null)
                    break;
            }
            if (fileUrl == null)
                throw new NotFoundException();

            Resource resFile = getFile(WebTopApp.get(request), fileUrl);
            return new StaticFile(fileUrl.toString(), getMimeType(remainingPath), ClientCaching.YES, resFile);

        } catch (ForbiddenException ex) {
            return new Error(HttpServletResponse.SC_FORBIDDEN, "Forbidden");
        } catch (NotFoundException ex) {
            return new Error(HttpServletResponse.SC_NO_CONTENT, "Not Content");
        } catch (InternalServerException ex) {
            return new Error(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "Internal server error");
        } catch (ServletException ex) {
            return new Error(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, ex.getMessage());
        }
    }

    private LookupResult lookupWhatsnew(HttpServletRequest request, URL targetUrl, String path, String serviceId) {
        URL fileUrl = null;

        try {
            String resPath = "/" + LangUtils.packageToPath(serviceId) + "/meta/" + path;
            fileUrl = getResURL(resPath);
            if (fileUrl == null)
                throw new NotFoundException();

            Resource resFile = getFile(WebTopApp.get(request), fileUrl);
            return new StaticFile(fileUrl.toString(), getMimeType(path), ClientCaching.YES, resFile);

        } catch (ForbiddenException ex) {
            return new Error(HttpServletResponse.SC_FORBIDDEN, "Forbidden");
        } catch (NotFoundException ex) {
            return new Error(HttpServletResponse.SC_NO_CONTENT, "Not Content");
        } catch (InternalServerException ex) {
            return new Error(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "Internal server error");
        } catch (ServletException ex) {
            return new Error(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, ex.getMessage());
        }
    }

    private LookupResult lookupDefault(HttpServletRequest request, ClientCaching clientCaching, URL url) {
        //logger.trace("Looking-up file as default");
        String path = url.getPath();
        URL fileUrl = null;

        try {
            fileUrl = this.getClass().getResource(path);
            Resource resFile = getFile(WebTopApp.get(request), fileUrl);
            return new StaticFile(fileUrl.toString(), getMimeType(path), clientCaching, resFile);

        } catch (ForbiddenException ex) {
            return new Error(HttpServletResponse.SC_FORBIDDEN, "Forbidden");
        } catch (NotFoundException ex) {
            return new Error(HttpServletResponse.SC_NOT_FOUND, "Not Found");
        } catch (InternalServerException ex) {
            return new Error(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "Internal server error");
        } catch (ServletException ex) {
            return new Error(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, ex.getMessage());
        }
    }

    private Resource getFile(WebTopApp wta, URL url) throws ResourceRequest.ForbiddenException,
            ResourceRequest.NotFoundException, ResourceRequest.InternalServerException {
        Resource resource = null;
        if (url == null)
            throw new ResourceRequest.NotFoundException();

        String protocol = url.getProtocol();
        if (protocol.equals("file")) {
            try {
                resource = wta.getFileResource(url);
            } catch (URISyntaxException | MalformedURLException | WTRuntimeException ex) {
                throw new ResourceRequest.InternalServerException();
            }
        } else if (protocol.equals("jar")) {
            try {
                resource = wta.getJarResource(url);
            } catch (URISyntaxException | MalformedURLException | WTRuntimeException ex) {
                throw new ResourceRequest.InternalServerException();
            } catch (IOException ex) {
                throw new ResourceRequest.NotFoundException();
            }
        } else {
            throw new ResourceRequest.InternalServerException();
        }

        if (resource == null)
            throw new ResourceRequest.NotFoundException();
        return resource;
    }

    public class NotFoundException extends Exception {
        public NotFoundException() {
            super();
        }
    }

    public class InternalServerException extends Exception {
        public InternalServerException() {
            super();
        }
    }

    public class ForbiddenException extends Exception {
        public ForbiddenException() {
            super();
        }
    }

    protected String getPath(HttpServletRequest request) {
        return StringUtils.defaultString(request.getPathInfo());
    }

    protected boolean isForbidden(String path) {
        String lpath = path.toLowerCase();
        return lpath.startsWith("/web-inf/") || lpath.startsWith("/meta-inf/");
    }

    protected String getMimeType(String path) {
        return LangUtils.coalesce(getServletContext().getMimeType(path), "application/octet-stream");
    }

    public static interface LookupResult {
        public boolean cacheLastModified();

        public long getLastModified();

        public void respondGet(HttpServletRequest request, HttpServletResponse response) throws IOException;

        public void respondHead(HttpServletRequest request, HttpServletResponse response);
    }

    public enum ClientCaching {
        YES, AUTO, NO
    }

    public static class StaticFile implements LookupResult {
        protected boolean cacheLastModified;
        protected final String url;
        protected final String mimeType;
        protected final ClientCaching clientCaching;
        protected final String charset;
        protected final Resource resourceFile;

        public StaticFile(String url, String mimeType, ClientCaching clientCaching, Resource resourceFile) {
            this(url, mimeType, clientCaching, null, resourceFile);
        }

        public StaticFile(String url, String mimeType, ClientCaching clientCaching, String charset,
                Resource resourceFile) {
            this.cacheLastModified = true;
            this.url = url;
            this.mimeType = mimeType;
            this.clientCaching = clientCaching;
            this.charset = charset;
            this.resourceFile = resourceFile;
        }

        @Override
        public boolean cacheLastModified() {
            return cacheLastModified;
        }

        @Override
        public long getLastModified() {
            return resourceFile.getLastModified();
        }

        @Override
        public void respondGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
            OutputStream os = null;
            InputStream is = null;

            try {
                prepareContent();
                ServletUtils.setContentTypeHeader(response, mimeType);
                if (clientCaching.equals(ClientCaching.YES)) {
                    ServletUtils.setCacheControlPrivateMaxAge(response, 60 * 60 * 24 * 365); // long (365 days)
                } else {
                    if (clientCaching.equals(ClientCaching.NO)) {
                        ServletUtils.setCacheControlPrivateNoCache(response);
                    } else {
                        if (StringUtils.startsWith(mimeType, "image")
                                || StringUtils.startsWith(mimeType, "text/css")) {
                            ServletUtils.setCacheControlPrivateMaxAge(response, 60 * 60 * 24); // 1 day
                        } else {
                            ServletUtils.setCacheControlPrivateNoCache(response);
                        }
                    }
                }
                os = ServletUtils.prepareForStreamCopy(request, response, mimeType, getContentLength(),
                        ServletUtils.GZIP_MIN_THRESHOLD);
                is = getInputStream();
                ServletUtils.transferStreams(is, os);
                os.flush();
                response.setStatus(HttpServletResponse.SC_OK);
            } finally {
                IOUtils.closeQuietly(os);
                IOUtils.closeQuietly(is);
            }
        }

        @Override
        public void respondHead(HttpServletRequest request, HttpServletResponse response) {
        }

        protected void prepareContent() throws IOException {
        }

        protected InputStream getInputStream() throws IOException {
            return resourceFile.getInputStream();
        }

        protected int getContentLength() {
            return (int) resourceFile.getSize();
        }
    }

    public static class LocaleJsFile extends StaticFile {
        protected String clazz;
        protected String override;
        protected String json = null;
        protected int contentLength = -1;

        public LocaleJsFile(String clazz, String override, String url, ClientCaching clientCaching,
                Resource resourceFile) {
            super(url, "application/javascript", clientCaching, "utf-8", resourceFile);
            this.clazz = clazz;
            this.override = override;
        }

        @Override
        protected void prepareContent() throws IOException {
            InputStream is = resourceFile.getInputStream();
            String strings = loadProperties(is);
            IOUtils.closeQuietly(is);
            is = null;

            json = buildLocaleJson(clazz, strings);
            contentLength = json.getBytes().length;
        }

        @Override
        public InputStream getInputStream() throws IOException {
            if (json == null)
                throw new WTRuntimeException("This method needs to be called after prepareContent()");
            return IOUtils.toInputStream(json, WT.getSystemCharset());
        }

        @Override
        protected int getContentLength() {
            if (contentLength == -1)
                throw new WTRuntimeException("This method needs to be called after prepareContent()");
            return contentLength;
        }

        private String buildLocaleJson(String clazz, String strings) {
            return "Ext.define('" + clazz + "',{" + "strings:" + strings + "});";
        }

        private String loadProperties(InputStream is) {
            ArrayList<String> strings = new ArrayList();
            BufferedReader br = null;

            try {
                /*
                Properties properties = new Properties();
                properties.load(new InputStreamReader(is, "UTF-8"));
                for(final String name: properties.stringPropertyNames()) {
                   final String s = "\"" + name + "\"" + ":" + "\"" + properties.getProperty(name) + "\"";
                   strings.add(s);
                }
                */

                PropertiesEx properties = new PropertiesEx();
                properties.load(is, true); // Important! True to preserve unicode escapes found in properties
                for (final String name : properties.stringPropertyNames()) {
                    //TODO: Si pu forse applicare? LangUtils.escapeJsonDoubleQuote -> StringEscapeUtils.escapeJson
                    final String s = "\"" + name + "\"" + ":" + "\""
                            + LangUtils.escapeJsonDoubleQuote(properties.getProperty(name)) + "\"";
                    strings.add(s);
                }

                // Alternative way to preserve escapes...
                /*
                br = new BufferedReader(new InputStreamReader(is, Charset.forName("ISO-8859-1")));
                String line = null, key, value, json;
                int firstEqual = -1;
                while((line = br.readLine()) != null) {
                   firstEqual = line.indexOf("=");
                   if(firstEqual > 0) {
                      key = line.substring(0, firstEqual);
                      value = line.substring(firstEqual+1);
                      json = "\"" + key + "\"" + ":" + "\"" + value + "\"";
                      strings.add(json);
                   }
                }
                */

            } catch (IOException ex) {
                throw new RuntimeException(ex);
            } finally {
                IOUtils.closeQuietly(br);
            }

            return "{" + StringUtils.join(strings, ",") + "}";
        }
    }

    public static class Error implements LookupResult {
        protected final int statusCode;
        protected final String message;

        public Error(int statusCode, String message) {
            this.statusCode = statusCode;
            this.message = message;
        }

        @Override
        public boolean cacheLastModified() {
            return false;
        }

        @Override
        public long getLastModified() {
            return -1;
        }

        @Override
        public void respondGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
            response.sendError(statusCode, message);
        }

        @Override
        public void respondHead(HttpServletRequest request, HttpServletResponse response) {
            throw new UnsupportedOperationException();
        }
    }
}