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