package ir.xweb.module; import ir.xweb.data.DataTools; import ir.xweb.server.XWebUser; import ir.xweb.util.MimeType; import ir.xweb.util.Tools; import ir.xweb.util.XmlBundle; import org.apache.commons.fileupload.FileItem; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.xml.sax.InputSource; import javax.servlet.ServletContext; import javax.servlet.ServletException; import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.transform.Transformer; import javax.xml.transform.TransformerFactory; import javax.xml.transform.dom.DOMSource; import javax.xml.transform.stream.StreamResult; import javax.xml.transform.stream.StreamSource; import java.io.*; import java.nio.MappedByteBuffer; import java.nio.channels.Channels; import java.nio.channels.FileChannel; import java.nio.channels.WritableByteChannel; import java.nio.file.Files; import java.sql.Date; import java.text.SimpleDateFormat; import java.util.*; public class ResourceModule extends Module { private final static Logger logger = LoggerFactory.getLogger("ResourceModule"); private final SimpleDateFormat dateFormat = new SimpleDateFormat("EEE, dd MMM yyyy hh:mm:ss zzz", Locale.ENGLISH); private final static String DEFAULT_BUNDLE = "bundle"; private long startTempNumber = System.currentTimeMillis(); /** * Temp directory. It can be absolute or relative path. If you use relative path * will be parent folder */ public final static String PROPERTY_TEMP_DIR = "dir.temp"; public final static String PROPERTY_DEFAULT_ID = "default.id"; public final static String PROPERTY_DATA_DIR = "dir.data"; public final static String PROPERTY_TEMPLATE_DIR = "dir.template"; public final static String PROPERTY_RESOURCE_BUNDLE_DIR = "dir.bundle"; public final static String PROPERTY_STORE_PATTERN = "store.pattern"; public final static String PROPERTY_DEFAULT_LANGUAGE = "default.language"; public final static String MODE_PUBLIC = "public"; public final static String MODE_PROTECTED = "protected"; public final static String MODE_PRIVATE= "private"; private final String tempDirPath; /** * You should not use this field directly, maybe it's not or directory does no exist anymore. * We can use {@link #getTempDir() getTempDir()} instead */ private File tempDir; private final File dataDir; private final File templateDir; /** Remember bundle dir can be NULL **/ private final File bundleDir; private final String defaultId; private final String storePattern; private final String defaultLanguage; private ServletContext context; private final Map<String, XmlBundle> bundles = new HashMap<String, XmlBundle>(); public ResourceModule( final Manager manager, final ModuleInfo info, final ModuleParam properties) throws ModuleException { super(manager, info, properties); // create and init temp dir this.tempDirPath = properties.getString(PROPERTY_TEMP_DIR, null); // We don't wanna create temp dir when we don't need it this.dataDir = properties.getFile(PROPERTY_DATA_DIR, getTempDirFromSystem()); if((!this.dataDir.exists() && !this.dataDir.mkdirs()) || !this.dataDir.canWrite()) { throw new IllegalArgumentException("Data is not accessible: " + this.dataDir); } this.templateDir = properties.getFile(PROPERTY_TEMPLATE_DIR, new File(manager.getContext().getRealPath("template"))); // set bundle directory and load default bundle this.bundleDir = properties.getFile(PROPERTY_RESOURCE_BUNDLE_DIR, new File(manager.getContext().getRealPath("bundle"))); final File defaultBundle = new File(DEFAULT_BUNDLE, DEFAULT_BUNDLE + ".xml"); if(defaultBundle.exists()) { try { bundles.put(DEFAULT_BUNDLE, new XmlBundle(defaultBundle)); } catch (Exception ex) { new IllegalArgumentException("Illegal default bundle file: " + defaultBundle, ex); } } this.storePattern = properties.getString(PROPERTY_STORE_PATTERN, null); // deprecated this.defaultId = properties.getString(PROPERTY_DEFAULT_ID, null); this.defaultLanguage = properties.getString( PROPERTY_DEFAULT_LANGUAGE, getManager().getProperty(PROPERTY_DEFAULT_LANGUAGE)); } @Override public void init(final ServletContext context) { this.context = context; } @Override public void process( final ServletContext context, final HttpServletRequest request, final HttpServletResponse response, final ModuleParam params, final Map<String, FileItem> files) throws IOException { final XWebUser user = (XWebUser)request.getSession().getAttribute(ir.xweb.server.Constants.SESSION_USER); if(params.containsKey("download")) { final String path = params.getString("download", null); final String id = params.getString("id", null); File file = null; if(id == null) { if(user == null) { file = getFile(path); } else { file = getFile(user, path); // if file not found for current user, try to get it from default user if(file == null) { file = getFile(path); } } } else { if(path.startsWith(MODE_PUBLIC + File.separator)) { file = getFile(id, path); } else if(path.startsWith(MODE_PROTECTED + File.separator)) { if(user != null) { file = getFile(id, path); } else { logger.info("Try to access resource that is protected by user session is not active: " + path); } } else if(isAdmin(user)) { file = getFile(id, path); } else { logger.info("Try to access resource that is private by user session is not active: " + path); } } if(file == null || !file.exists()) { // we do not rise exception because it's not important to log this message throw new ModuleException(HttpServletResponse.SC_NOT_FOUND, "Resource not found: " + path); } else { // check to store boolean store = false; //DataSource dataSource = getManager().getDataSource("data-resource"); //if(dataSource != null) { if(storePattern != null && path.matches(storePattern)) { if (request.getHeader("Range") != null) { // record it just for the first section of file store = request.getHeader("Range").startsWith("bytes=0") || request.getHeader("Range").startsWith("bytes=-"); } else { store = true; } } //} try { // store count if(store) { storeResourceUsage(path); //dataSource.setData(context, ResourceDataSource.DATA_SOURCE_STORE, path); } if (request.getHeader("Range") != null) { rangeDownload(request, response, file); } else { fullDownload(request, response, file); } } catch (Exception ex) { throw new ModuleException("", ex); } } } } private void rangeDownload(final HttpServletRequest request, final HttpServletResponse response, final File file) throws ServletException, IOException { response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT); final ArrayList<Long> rawRangesList = new ArrayList<Long>(); final String range = request.getHeader("Range"); if(!range.startsWith("bytes=")){ response.sendError(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE); return; } try { final StringTokenizer tokenizer = new StringTokenizer(range.substring("bytes=".length()), ","); String token; while(tokenizer.hasMoreTokens()){ token = tokenizer.nextToken(); rawRangesList.add(getRangeStart(token)); rawRangesList.add(getRangeEnd(token)); } } catch (NumberFormatException e) { response.sendError(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE); return; } if(rawRangesList.size()==0){ response.sendError(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE); return; } //calculate size and validate values final long fileSize = file.length(); final long validRanges [] = new long[rawRangesList.size()]; long totalSize = 0; for (int i = 0; i < rawRangesList.size()/2; i++) { final Long start = rawRangesList.get(i); final Long end = rawRangesList.get(i + 1); if(start==null&&end==null){ response.sendError(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE); return; } final long from; final long till; if (start == null) { from = fileSize - end.longValue(); till = fileSize; } else if (end == null) { from = start.longValue(); till = fileSize; } else { from = start.longValue(); till = end.longValue() + 1; } if( from >= till || from < 0 || till < 0 ||till > fileSize){ response.sendError(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE); return; } validRanges[i] = from; validRanges[i+1] = till; totalSize += till - from; } //set headers response.setHeader("Accept-Ranges", "bytes"); response.setHeader("ETag", getETag(file)); response.setHeader("Last-Modified", getLastModified(file)); response.setHeader("Content-Length", String.valueOf(totalSize)); response.setHeader("Content-Type", MimeType.get(file)); response.setHeader("Content-Range", getContentRange(validRanges, fileSize)); response.setHeader("Content-Disposition", "inline; " + "filename=" + file.getName() + "; " + "modification-date=\"" + dateFormat.format(file.lastModified()) + "\""); //send body response.setBufferSize(0); final OutputStream out = response.getOutputStream(); if (!"head".equalsIgnoreCase(request.getMethod())) { for (int i = 0; i < validRanges.length / 2; i++) { write(file, out, validRanges[i], validRanges[i + 1]); } } } private String getContentRange(final long [] ranges, final long fileSize) { //TODO return "bytes " + ranges[0] + "-" + (ranges[1]-1) + "/" + fileSize; } public File getFile(final String path) { if(this.defaultId == null) { throw new IllegalArgumentException("Default identifier not found. Please set: " + PROPERTY_DEFAULT_ID); } return getFile(this.defaultId, path); } public File getFile(final String id, final String path) { if(id == null) { throw new IllegalArgumentException("null id"); } if(path == null) { throw new IllegalArgumentException("null path"); } if(path.matches("^(\\./|\\.\\./|~/|\\\\)")) { // try to hack throw new IllegalArgumentException("Illegal path: " + path); } File f = new File(this.dataDir, getUserDirectory(id) + File.separator + path); if(f.exists()) { return f; } return null; } public File getFile(final XWebUser user, final String path) { return getFile(user.getId(), path); } public File initTempFile() throws IOException { File file; do { file = new File(getTempDir(), Long.toString(++startTempNumber)); } while (file.exists()); file.createNewFile(); return file; } /** * Create temp file and write inputstream on that * @param is * @return */ public File initTempFile(InputStream is) throws IOException { if(is == null) { throw new IllegalArgumentException("null stream"); } final File file = initTempFile(); Files.copy(is, file.toPath()); return file; } /** * Create Unique directory inside temp directory * @return * @throws IOException */ public File initTempDir() throws IOException { File file; do { file = new File(getTempDir(), Long.toString(++startTempNumber)); } while (file.exists()); if(!file.mkdirs()) { throw new IOException("Can not create temp dir. Please set '" + PROPERTY_TEMP_DIR + "' property. " + file); } return file; } /** * Write file with gzip checking, file <filepath>.gz exist and request support gz file, we will use GZ instead of * original file * @param request * @param response * @param file * @throws IOException */ public void writeFile( final HttpServletRequest request, final HttpServletResponse response, final File file) throws IOException { if(file == null) { throw new IllegalArgumentException("Null file"); } boolean zipSupport = false; final File zipFile = new File(file.getPath() + ".gz"); if(!file.exists() && !zipFile.exists()) { throw new ModuleException(HttpServletResponse.SC_NOT_FOUND, file + " not found"); } if(!response.containsHeader("Content-Encoding")) { if(zipFile.exists() && zipFile.canRead()) { final String encoding = request.getHeader("Accept-Encoding"); if(encoding != null && encoding.toLowerCase().indexOf("gzip") > -1) { zipSupport = true; } } } if(zipSupport) { response.addHeader("Content-Encoding", "gzip"); if(!response.containsHeader("Content-Type")) { response.addHeader("Content-Type", MimeType.get(file)); } writeFile(response, zipFile); } else { writeFile(response, file); } } public void writeFile(final HttpServletResponse response, final File file) throws IOException { if(file != null && file.exists()) { if(response == null) { throw new IllegalArgumentException("null response"); } final long from = 0; final long till = file.length(); // set headers response.setHeader("Accept-Ranges", "bytes"); response.setHeader("ETag", getETag(file)); response.setHeader("Last-Modified", getLastModified(file)); response.setHeader("Content-Length", String.valueOf(till - from)); response.setHeader("Content-Disposition", "inline; " + "filename=" + file.getName() + "; " + "modification-date=\"" + dateFormat.format(file.lastModified()) + "\""); if(!response.containsHeader("Content-Type")) { response.addHeader("Content-Type", MimeType.get(file)); } response.setBufferSize(0); final OutputStream out = response.getOutputStream(); try { write(file, out, from, till); } catch (ServletException ex) { throw new IOException(ex); } } else { throw new ModuleException(HttpServletResponse.SC_NOT_FOUND, (file == null ? null : file.getName()) + " not found"); } } public File initResourceDir(final String path) { if(this.defaultId == null) { throw new IllegalArgumentException("Default identifier not found"); } return initResourceDir(this.defaultId, path); } public File getDataDir() { return dataDir; } /** * Get temp directory. If temp directory removed by system, it will make new one. * @return */ public File getTempDir() { if(this.tempDir == null || !this.tempDir.exists()) { if(this.tempDirPath != null) { final File d = new File(this.tempDirPath); if(d.isAbsolute()) { this.tempDir = d; } else { this.tempDir = new File(this.dataDir, d.getPath()); } } else { this.tempDir = getTempDirFromSystem(); } // create temp dir this.tempDir.mkdirs(); } return this.tempDir; } public File getTemplateDir() { return this.templateDir; } private File getTempDirFromSystem() { try { return Files.createTempDirectory("" + System.currentTimeMillis()).toFile(); } catch (Exception ex) { return new File( System.getProperty("java.io.tmpdir") + File.separator + System.currentTimeMillis()); } } public File initResourceDir(String id, String path) { File dir = new File(dataDir, id + File.separator + path); if(dir.exists() || dir.mkdirs()) { return dir; } return null; } public File initResourceFile(String userId, String path) { File dir = new File(dataDir, userId); File file = new File(dir, path); File p = file.getParentFile(); if(!p.exists() && !p.mkdirs()) { return null; } return file; } /*public File initResourceDir(XWebUser user, String path) { File dir = new File(dataDir, user.getId() + File.separator + path); if(dir.exists() || dir.mkdirs()) { return dir; } return null; }*/ /** * Write partial file data * @param file * @param out * @param from * @param till * @throws ServletException * @throws IOException */ private void write(final File file, final OutputStream out, final long from, final long till) throws ServletException, IOException { final RandomAccessFile f = new RandomAccessFile(file, "r"); final WritableByteChannel outChannel = Channels.newChannel(out); try { final MappedByteBuffer map = f.getChannel().map(FileChannel.MapMode.READ_ONLY, from, till - from); outChannel.write(map); } finally { if(f != null) { try { f.close(); } catch (Exception ex) {} } } } private String getETag(final File file) { return "W/\"" + file.length() + "-" + file.lastModified() + "\""; } private String getLastModified(final File file) { return this.dateFormat.format(new Date(file.lastModified())); } private void fullDownload( final HttpServletRequest request, final HttpServletResponse response, final File file) throws ServletException, IOException { final long from = 0; final long till = file.length(); // set headers response.setHeader("Accept-Ranges", "bytes"); response.setHeader("ETag", getETag(file)); response.setHeader("Last-Modified", getLastModified(file)); response.setHeader("Content-Length", String.valueOf(till - from)); response.setHeader("Content-Type", MimeType.get(file)); response.setHeader("Content-Disposition", "inline; " + "filename=" + file.getName() + "; " + "modification-date=\"" + dateFormat.format(file.lastModified()) + "\""); response.setBufferSize(0); final OutputStream out = response.getOutputStream(); if(!"head".equalsIgnoreCase(request.getMethod())){ write(file, out, from, till); } } private Long getRangeStart(final String rangeInterval) throws NumberFormatException{ final int index = rangeInterval.indexOf("-"); if(index < 0){ throw new NumberFormatException(); } final String value = rangeInterval.substring(0, index); if(value.length() == 0){ return null; } return new Long(value); } private Long getRangeEnd(final String rangeInterval)throws NumberFormatException{ final int index = rangeInterval.indexOf("-"); if(index < 0){ throw new NumberFormatException(); } final String value = rangeInterval.substring(index+1); if(value.length() == 0){ return null; } return new Long(value); } /** * Apply template to XML string * @param template Name of template in template dir * @param language Language of template (null for default) * @param xml XML String * @return Transformed result */ public String applyXmlTemplate( final String template, final String language, final String xml) { return applyXmlTemplate(template, language, null, xml); } /** * Apply template to XML string * @param template Name of template in template dir * @param language Language of template (null for default) * @param parameters XSLT extra parameters OR NULL * @param xml XML String greyhounds * @return Transformed result */ public String applyXmlTemplate( final String template, final String language, final Map<String, ?> parameters, final String xml) { // find XSLT template final File xsltFile = getTemplateFile(template, language, ".xsl"); if(xsltFile != null) { try { final String html = applyXslt(xsltFile, xml, parameters); return html; } catch (Exception ex) { logger.error("Error to apply XSLT template: " + xsltFile); } } return null; } public String applyTemplate( final String template, final String language, final Map<String, ?> requestParams) { return applyTemplate(template, language, requestParams, null); } /** * Apply XSLT or TEXT template to request parameters * @param template Template name in template folder * @param language Language of template (null for default) * @param requestParams Request parameters * @param xsltParams XSLT extra parameters OR NULL * @return */ public String applyTemplate( final String template, final String language, final Map<String, ?> requestParams, final Map<String, ?> xsltParams) { // find XSLT template final File xsltFile = getTemplateFile(template, language, ".xsl"); if(xsltFile != null) { try { final DataTools dataTools = new DataTools(); final String xml = dataTools.write("xml", null, requestParams); final String html = applyXslt(xsltFile, xml, xsltParams); return html; } catch (Exception ex) { logger.error("Error to apply XSLT template: " + xsltFile); } } // find text template final File textFile = getTemplateFile(template, language, ".txt"); if(textFile != null) { try { final String text = applyText(textFile, requestParams); return text; } catch (Exception ex) { logger.error("Error to apply XSLT template: " + xsltFile); } } return null; } private String applyText(final File template, final Map<String, ?> params) throws Exception { String text = Tools.readTextFile(template); for(String key:params.keySet()) { final Object value = params.get(key); text = text.replace("${" + key + "}", value == null ? null : value.toString()); } return text; } private String applyXslt( final File template, final String xml, final Map<String, ?> xltParams) throws Exception { final DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); final DocumentBuilder builder = factory.newDocumentBuilder(); final InputSource is = new InputSource(); is.setCharacterStream(new StringReader(xml)); // Use a Transformer for output final TransformerFactory tFactory = TransformerFactory.newInstance(); final StreamSource styleSource = new StreamSource(template); final Transformer transformer = tFactory.newTransformer(styleSource); if(xltParams != null) { for(Map.Entry<String, ?> e:xltParams.entrySet()) { if(e.getValue() == null) { throw new IllegalArgumentException("Null XLST parameter value for: " + e.getKey()); } transformer.setParameter(e.getKey(), e.getValue()); } } final StringWriter w = new StringWriter(); final DOMSource source = new DOMSource(builder.parse(is)); final StreamResult result = new StreamResult(w); transformer.transform(source, result); return w.toString(); } /*private String paramToXml(final Map<String, String> params) throws IOException { final Element root = new Element("params"); for(Map.Entry<String, String> e:params.entrySet()) { final Element param = new Element(e.getKey()); param.setText(e.getValue()); root.addContent(param); } final XMLOutputter xmlOutput = new XMLOutputter(); final StringWriter w = new StringWriter(); xmlOutput.setFormat(Format.getRawFormat()); xmlOutput.output(new Document(root), w); return w.toString(); }*/ private File getTemplateFile( final String template, final String language, final String extension) { File xsltFile = null; // find XSLT file if(language != null) { // ex: template-fa_IR.xsl xsltFile = new File(templateDir, template + "-" + language + extension); if(!xsltFile.exists()) { // ex: template-fa.xsl xsltFile = new File(templateDir, template + "-" + language.split("[_-]")[0] + extension); } } if(xsltFile == null || !xsltFile.exists()) { xsltFile = new File(templateDir, template + extension); } return xsltFile.exists() ? xsltFile : null; } public String getLanguage(final HttpServletRequest request) { // Get language from parameter String language = null; if(request != null) { language = request.getParameter("language"); // Get language from cookie if(language == null) { final Cookie[] cookies = request.getCookies(); if(cookies != null) { for(Cookie c:cookies) { if("language".equals(c.getName())) { language = c.getValue(); break; } } } } // get language from user-agent if(language == null) { final Enumeration locales = request.getLocales(); while (locales.hasMoreElements()) { Locale locale = (Locale) locales.nextElement(); language = locale.getDisplayLanguage(); break; } } } // default language if(language == null) { language = defaultLanguage; } // default server language if(language == null) { language = Locale.getDefault().getDisplayLanguage(); } return language; } public String getString(final Locale locale, final String key, final String defaultValue) { return getString(locale == null ? null : locale.getDisplayLanguage(), key, defaultValue); } public String getString(final String language, final String key, final String defaultValue) { final String l = language == null ? defaultLanguage : language; // try to load bundle if is not loaded XmlBundle b = bundles.get(l); if(b == null) { final File f = new File(this.bundleDir, l + ".xml"); if(f.exists()) { try { b = new XmlBundle(f); bundles.put(l, b); } catch (Exception ex) { logger.error("Error to load bundle file (" + l + "): " + f); } } } final String result = b == null ? defaultValue : b.getString(key); return result == null ? key : result; } protected boolean isAdmin(final XWebUser user) { return false; } protected void storeResourceUsage(final String path) { } protected String getUserDirectory(final String id) { return id; } }