JavaXT
|
|
WebSite Classpackage javaxt.express.cms; import javaxt.express.FileManager; import javaxt.express.utils.WebUtils; import javaxt.http.servlet.*; import javaxt.utils.Console; import java.io.IOException; import java.util.*; //****************************************************************************** //** WebSite Servlet //****************************************************************************** /** * Servlet used to serve up files and images for a website. HTML pages are * assembled on-the-fly using an HTML template and content files. Keywords * in the content files and template are substituted at runtime. Assembled * files are cached by clients using last modified dates. * ******************************************************************************/ public abstract class WebSite extends HttpServlet { protected static Console console = new Console(); private javaxt.io.Directory web; private FileManager fileManager; private javaxt.io.File template; private Tabs tabs; private String companyName; private String companyAcronym; private String author; private String keywords; private Redirects redirects; private String[] fileExtensions = new String[]{ ".html", ".txt" }; private String[] DefaultFileNames = new String[]{ "home", "index", "Overview" }; //************************************************************************** //** Constructor //************************************************************************** /** Used to instantiate the website. * * @param web Directory that contains html files, css, javascript, images, * etc. Assumes the template, tabs, and redirects are found in the style * folder. * * @param servletPath URL path to the website (relative to the hostname). */ public WebSite(javaxt.io.Directory web, String servletPath){ this.web = web; this.template = new javaxt.io.File(web + "style/template.html"); this.tabs = new Tabs(new javaxt.io.File(web + "style/tabs.txt")); this.redirects = new Redirects(new javaxt.io.File(web + "style/redirects.txt")); setServletPath(servletPath); this.fileManager = new FileManager(web); } //************************************************************************** //** Constructor //************************************************************************** public WebSite(javaxt.io.Directory web){ this(web, "/"); } //************************************************************************** //** getWebDirectory //************************************************************************** public javaxt.io.Directory getWebDirectory(){ return web; } //************************************************************************** //** getFileManager //************************************************************************** public FileManager getFileManager(){ return fileManager; } //************************************************************************** //** setCompanyName //************************************************************************** public void setCompanyName(String companyName){ this.setCompanyName(companyName, null); } public void setCompanyName(String companyName, String companyAcronym){ this.companyName = companyName; this.companyAcronym = companyAcronym; } //************************************************************************** //** setAuthor //************************************************************************** public void setAuthor(String author){ this.author = author; } //************************************************************************** //** getCopyright //************************************************************************** /** Returns the copyright text (e.g. "Copyright © 2012"). Classes that * extend this class can override this method. */ protected String getCopyright(){ return "Copyright © " + getYear(); } //************************************************************************** //** getYear //************************************************************************** protected int getYear(){ return new javaxt.utils.Date().getYear(); } //************************************************************************** //** processRequest //************************************************************************** public void processRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { long t = System.currentTimeMillis(); //Redirect as needed java.net.URL url = request.getURL(); if (redirect(url, response)) return; //Upgrade to HTTPS if we can... if (this.supportsHttps()){ response.setHeader("Content-Security-Policy", "upgrade-insecure-requests"); String upgradeRequest = request.getHeader("Upgrade-Insecure-Requests"); if (upgradeRequest!=null && upgradeRequest.equals("1")){ if (!url.getProtocol().equalsIgnoreCase("https")){ String location = url.toString(); location = "https" + location.substring(location.indexOf(":")); response.setStatus(307); //response.setHeader("Strict-Transport-Security", "max-age=31536000; includeSubDomains"); response.setHeader("Vary", "Upgrade-Insecure-Requests"); response.setHeader("Location", location); return; } } } //Get path from URL, excluding servlet path and leading "/" character String path = getPath(url); //Special case for Certbot. When generating certificates using the //certonly command, Certbot creates a hidden directory in the web root. //The web server must return the files in this hidden directory. However, //the filemanager does not allow access to hidden directories so we need //to handle these requests manually. if (path.startsWith(".well-known")){ console.log(path); java.io.File file = new java.io.File(web + path); console.log(file + "\t" + file.exists()); //Send file if (file.exists()){ response.write(file, javaxt.io.File.getContentType(file.getName()), true); } else{ response.setStatus(404); response.setContentType("text/plain"); } return; } //Send static file if we can javaxt.io.File file = getFile(path); if (file!=null){ //Check whether the file ends in a ".html" or ".txt" extension. If so, //check whether the file is static or if needs to be wrapped //in a template. boolean sendFile = true; String ext = file.getExtension().toLowerCase(); if (ext.equals("html")){ //Don't send html files unless they end with a </html> tag sendFile = !isSnippet(file); } else if (ext.equals("txt")){ //Don't send text files from the wiki directory String filePath = file.getDirectory().toString(); int idx = filePath.indexOf("/wiki/"); sendFile = (idx==-1); } if (sendFile){ sendFile(file, fileManager, request, response); return; } } else{ //Check whether the url path ends with a file extension. Return an error int idx = path.lastIndexOf("/"); if (idx>-1) path = path.substring(idx); idx = path.lastIndexOf("."); if (idx>-1){ //console.log(path); response.sendError(404); return; } } //If we're still here, generate html response sendHTML(request, response); //console.log("processRequest", System.currentTimeMillis()-t); } //************************************************************************** //** sendFile //************************************************************************** /** Used to send a static file to the client (e.g. css, javascript, images, * zip files, etc). By default, this method simple calls the following: <pre> fileManager.sendFile(file, request, response); </pre> * * Callers can override this method and add additional logic (e.g. auditing, * authorization, logging, etc). */ protected void sendFile(javaxt.io.File file, FileManager fileManager, HttpServletRequest request, HttpServletResponse response) throws ServletException, java.io.IOException { fileManager.sendFile(file, request, response); } //************************************************************************** //** getPath //************************************************************************** /** Returns the path part of a url, excluding servlet path and leading "/" * character */ private String getPath(java.net.URL url){ String path = url.getPath(); String servletPath = getServletPath(); if (!servletPath.endsWith("/")) servletPath += "/"; path = path.substring(path.indexOf(servletPath)).substring(servletPath.length()); if (path.startsWith("/")) path = path.substring(1); return path; } //************************************************************************** //** getFile //************************************************************************** /** Returns a path to a static file (e.g. css, javascript, images, zip, etc) */ private javaxt.io.File getFile(String path){ //Restrict access to the "bin" directory if (path.toLowerCase().startsWith("bin/")){ return null; } //Construct a list of possible file paths ArrayList<String> files = new ArrayList<>(); files.add(path); files.add("downloads/" + path); //Loop through possible file combinations for (String str : files){ java.io.File file = fileManager.getFile(path); if (file!=null) return new javaxt.io.File(file); } return null; } //************************************************************************** //** sendHTML //************************************************************************** /** Used to construct an html document from a template and an html snippet. */ private void sendHTML(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { long t = System.currentTimeMillis(); String servletPath = getServletPath(); if (!servletPath.endsWith("/")) servletPath += "/"; //Get the html file java.net.URL url = request.getURL(); javaxt.io.File file = getHtmlFile(url); //<--Watch for NPE! //Check whether the client wants the raw file content or if we should //wrap the content in a template (default). boolean useTemplate = true; String templateParam = request.getParameter("template"); if (templateParam!=null){ if (templateParam.equals("false")){ useTemplate = false; } } //Calculate last modified date and estimated file fize TreeSet<Long> dates = new TreeSet<>(); if (file!=null) dates.add(file.getDate().getTime()); if (useTemplate){ dates.add(template.getDate().getTime()); dates.add(tabs.getLastModified()); } //Get content Content content = getContent(request, file); if (content==null){ content = new Content("404", new Date()); content.setStatusCode(404); } dates.add(content.getDate().getTime()); String html = content.getHTML(); //Update HTML. Note that there are currently two major bottlenecks here: //(1) html parser in "useTemplate" block and (2) the updateLinks method //Both can be mitigated with some simple caching long t0 = System.currentTimeMillis(); if (useTemplate){ //Instantiate html parser long t1 = System.currentTimeMillis(); javaxt.html.Parser document = new javaxt.html.Parser(html); //Extract Title String title = null; try{ javaxt.html.Element el = document.getElementByTagName("title"); html = html.replace(el.getOuterHTML(), ""); title = el.getInnerText().trim(); } catch(Exception e){} if (title==null){ try{ title = document.getElementByTagName("h1").getInnerHTML(); } catch(Exception e){} } if (title==null){ if (companyName!=null && companyAcronym!=null){ title = companyAcronym + " - " + companyName; } else{ if (file!=null){ title = file.getName(false); for (String fileName : DefaultFileNames){ if (title.equalsIgnoreCase(fileName)){ title = file.getDirectory().getName(); break; } } } } } if (title==null) title = ""; //Extract Description String description = null; try{ javaxt.html.Element el = document.getElementByTagName("description"); html = html.replace(el.getOuterHTML(), ""); description = el.getInnerHTML(); } catch(Exception e){} if (description==null) description = ""; //Extract Keywords String keywords = null; try{ javaxt.html.Element el = document.getElementByTagName("keywords"); html = html.replace(el.getOuterHTML(), ""); keywords = el.getInnerHTML(); } catch(Exception e){} if (keywords==null) keywords = this.keywords; if (keywords==null) keywords = ""; //console.log("parser", System.currentTimeMillis()-t1); html = template.getText().replace("<%=content%>", html); html = html.replace("<%=title%>", title); html = html.replace("<%=description%>", description); html = html.replace("<%=keywords%>", keywords); html = html.replace("<%=author%>", author==null ? "" : author); html = html.replace("<%=companyName%>", companyName==null ? "": companyName); html = html.replace("<%=year%>", getYear()+""); html = html.replace("<%=copyright%>", getCopyright()); html = html.replace("<%=tabs%>", getTabs(url.getPath(), tabs)); html = html.replace("<%=breadcrumbs%>", getBreadcrumbs(request)); html = html.replace("<%=sidebar%>", getSidebar(request)); html = html.replace("<%=Path%>", servletPath); html = updateLinks(html, dates, template); //console.log("useTemplate", System.currentTimeMillis()-t0); } else{ html = html.replace("<%=Path%>", servletPath); html = updateLinks(html, dates, file); //console.log("updateLinks", System.currentTimeMillis()-t0); } //Remove any orphan tags if (html.contains("<%=") && html.contains("%>")){ StringBuilder str = new StringBuilder(); String[] arr = html.split("<%="); for (int i=0; i<arr.length; i++){ String s = arr[i]; if (i>0){ int idx = s.indexOf("%>"); if (idx>-1) s = s.substring(idx+2); } str.append(s); } html = str.toString(); } //Trim the html html = html.trim(); //console.log("html", System.currentTimeMillis()-t); //Get last modified date long lastModified = dates.last(); String date = WebUtils.getDate(lastModified); //"EEE, dd MMM yyyy HH:mm:ss zzz" //Create eTag using the combined, uncompressed size of the html String eTag = "W/\"" + html.length() + "-" + lastModified + "\""; //Return 304/Not Modified response if we can... boolean useCache = true; if (useCache){ String matchTag = request.getHeader("if-none-match"); String cacheControl = request.getHeader("cache-control"); if (matchTag==null) matchTag = ""; if (cacheControl==null) cacheControl = ""; if (cacheControl.equalsIgnoreCase("no-cache")==false){ if (eTag.equalsIgnoreCase(matchTag)){ response.setStatus(304); return; } else{ //Internet Explorer 6 uses "if-modified-since" instead of "if-none-match" matchTag = request.getHeader("if-modified-since"); if (matchTag!=null){ for (String tag: matchTag.split(";")){ if (tag.trim().equalsIgnoreCase(date)){ response.setStatus(304); return; } } } } } } //Convert the html to a byte array byte[] rsp = html.getBytes("UTF-8"); //Set response headers response.setStatus(content.getStatusCode()); response.setCharacterEncoding("UTF-8"); response.setContentType("text/html"); response.setContentLength(rsp.length); response.setHeader("ETag", eTag); response.setHeader("Last-Modified", date); //Sat, 23 Oct 2010 13:04:28 GMT //Send response response.write(rsp); //console.log("sendHTML", System.currentTimeMillis()-t); } //************************************************************************** //** updateLinks //************************************************************************** /** Updates links in "script" and "link" tags with a querystring representing * the last modified date of the file. */ private String updateLinks(String html, TreeSet<Long> dates, javaxt.io.File htmlFile){ //Generate a list of supported tags HashMap<String, String> tagsWithLinks = new HashMap(); tagsWithLinks.put("script", "src"); tagsWithLinks.put("link", "href"); //Get elements that match the supported tags ArrayList<javaxt.html.Element> elements = new ArrayList<>(); javaxt.html.Parser document = new javaxt.html.Parser(html); Iterator<String> it = tagsWithLinks.keySet().iterator(); while (it.hasNext()){ String tagName = it.next(); String linkAttr = tagsWithLinks.get(tagName); for (javaxt.html.Element el : document.getElementsByTagName(tagName)){ String url = el.getAttribute(linkAttr); if (!(url==null || url.isEmpty())){ String t = url.toLowerCase(); if (!t.startsWith("http://") && !t.startsWith("https://") && !t.startsWith("//")){ elements.add(el); } } } } if (elements.isEmpty()) return html; //Generate an XML document StringBuilder str = new StringBuilder(); str.append("<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\r\n"); str.append("<links>"); for (javaxt.html.Element el : elements){ str.append("\r\n"); str.append(el.toString()); if (!el.isClosed()){ str.append("</" + el.getName() + ">"); } } str.append("\r\n</links>"); org.w3c.dom.Document xml = javaxt.xml.DOM.createDocument(str.toString()); //Update links in the XML try{ long lastUpdate = fileManager.updateLinks(htmlFile, xml); dates.add(lastUpdate); } catch(Exception e){ throw new RuntimeException(e); } //Update html document org.w3c.dom.Node outerNode = javaxt.xml.DOM.getOuterNode(xml); org.w3c.dom.Node[] nodes = javaxt.xml.DOM.getNodes(outerNode.getChildNodes()); for (int i=0; i<nodes.length; i++){ org.w3c.dom.Node node = nodes[i]; String orgTag = elements.get(i).getOuterHTML(); String newTag = javaxt.xml.DOM.getText(node); //Replace any self-enclosing script tags as needed String nodeName = node.getNodeName().toLowerCase(); if (newTag.endsWith("/>") && nodeName.equals("script")){ newTag = newTag.substring(0, newTag.length()-2); newTag += "></" + nodeName + ">"; } html = html.replace(orgTag, newTag); } return html; } //************************************************************************** //** getContent //************************************************************************** /** Returns an html snippet found in the given file. This method can be * overridden to generate dynamic content or to support custom tags. */ protected Content getContent(HttpServletRequest request, javaxt.io.File file){ if (file==null || !file.exists()){ return null; } else{ return new Content(file.getText("UTF-8"), file.getDate()); } } //************************************************************************** //** getHtmlFile //************************************************************************** /** Maps the requested URL to an html snippet found in an html or txt file. * Returns null if suitable a file is not found. */ private javaxt.io.File getHtmlFile(java.net.URL url){ //Get path from url String path = url.getPath(); String servletPath = getServletPath(); if (!servletPath.endsWith("/")) servletPath += "/"; path = path.substring(path.indexOf(servletPath)).substring(servletPath.length()); //Remove leading and trailing "/" characters if (path.startsWith("/")) path = path.substring(1); if (path.endsWith("/")) path = path.substring(0, path.length()-1); //Check whether the url points directly to a file (minus the file extension) //or if the url points to a directory. If so, return the file. String folderPath = web.toString(); javaxt.io.File file = getFile(path, folderPath); if (file!=null) return file; //If we are still here, check whether the url is missing a content folder //in its path (e.g. "documentation", "wiki"). String[] contentFolders = new String[]{"documentation", "wiki"}; for (String folderName : contentFolders){ folderPath = web + folderName + "/"; file = getFile(path, folderPath); if (file!=null) return file; } return null; } //************************************************************************** //** getFile //************************************************************************** private javaxt.io.File getFile(String path, String folderPath){ //Check whether the url points directly to a file (minus the file extension) if (path.length()>0){ //System.out.println("Checking: " + folderPath + path + ".*"); for (String fileExtension : fileExtensions){ javaxt.io.File file = new javaxt.io.File(folderPath + path + fileExtension); if (file.exists()){ if (isSnippet(file)) return file; } } } //Check whether the url points to a directory. If so, check whether the //directory has a welcome file (e.g. index.html, Overview.txt, etc). javaxt.io.Directory dir = new javaxt.io.Directory(folderPath + path); //System.out.println("Search: " + dir + " <--" + dir.exists()); if (dir.exists()){ for (String fileName : DefaultFileNames){ for (String fileExtension : fileExtensions){ javaxt.io.File file = new javaxt.io.File(dir, fileName + fileExtension); if (file.exists()){ if (isSnippet(file)) return file; } } } } return null; } //************************************************************************** //** isSnippet //************************************************************************** private boolean isSnippet(javaxt.io.File file){ String str = file.getText("UTF-8").trim(); return !str.endsWith("</html>"); } //************************************************************************** //** getIndex //************************************************************************** /** Returns an html snippet with paths to all the html/txt files found in * the given file path. Note that the file date is updated to reflect the * most current file. */ protected Content getIndex(javaxt.io.File file){ //Get relative path to the file javaxt.io.Directory dir = file.getDirectory(); String path = dir.toString(); path = path.substring(web.toString().length()); path = path.replace("\\", "/"); if (!path.startsWith("/")) path = "/" + path; if (!path.endsWith("/")) path += "/"; //Generate list of files and dates List<javaxt.io.File> files = new LinkedList<>(); TreeSet<Long> dates = new TreeSet<>(); dates.add(file.getDate().getTime()); for (javaxt.io.File f : dir.getFiles(fileExtensions, true)){ if (!f.equals(file)){ files.add(f); dates.add(f.getDate().getTime()); } } //Build table of contents using ul/li tags StringBuffer toc = new StringBuffer(); toc.append("<ul>\r\n"); String prevPath = ""; int len = dir.getPath().length(); Iterator<javaxt.io.File> it = files.iterator(); while (it.hasNext()){ javaxt.io.File f = it.next(); String fileName = f.getName(false); String relPath = f.getDirectory().getPath().substring(len).replace("\\", "/"); String link = path; if (relPath.length()>0){ link += relPath; } link += fileName; String li = "<li><a href=\"" + link + "\">" + fileName.replace("_", " ") + "</a></li>\r\n"; if (relPath.equals(prevPath)){ toc.append(li); } else{ String[] prevDirs = prevPath.split("/"); String[] currDirs = relPath.split("/"); //Close previous UL tags if (prevPath.length()>0){ //Compute number of tags to close int numTags = prevDirs.length; for (int i=0; i<prevDirs.length; i++){ String prevDir = prevDirs[i]; String currDir = (i<currDirs.length-1 ? currDirs[i] : ""); if (prevDir.equals(currDir)){ numTags--; } else{ break; } } //Close the tags for (int j=0; j<numTags; j++){ toc.append("</ul>\r\n"); } } //Compute number of tags to open int numTags = currDirs.length; if (prevPath.length()>0){ for (int i=0; i<currDirs.length; i++){ String currDir = currDirs[i]; String prevDir = (i<prevDirs.length-1 ? prevDirs[i] : ""); if (currDir.equals(prevDir)){ numTags--; } else{ break; } } } //Open new tags for (int i=0; i<numTags; i++){ int offset = (currDirs.length)-numTags; int idx = offset+i; String dirName = currDirs[idx]; String tag = null; if (idx==0){ tag = "h2"; } toc.append("<li>"); if (tag!=null) toc.append("<" + tag + ">"); toc.append(dirName.replace("_", " ")); if (tag!=null) toc.append("</" + tag + ">"); toc.append("</li>\r\n"); toc.append("<ul>\r\n"); } toc.append(li); prevPath = relPath; } //Close tags if (!it.hasNext()){ //Compute number of tags to close String[] currDirs = relPath.split("/"); int numTags = currDirs.length; //Close the tags for (int j=0; j<numTags; j++){ toc.append("</ul>\r\n"); } } } toc.append("</ul>\r\n"); //Update the date of the file to the most recent file in the directory Date lastModified = new Date(dates.last()); //if (!lastModified.equals(file.getDate())) System.out.println("Update file date: " + lastModified); //file.setDate(lastModified); return new Content(toc.toString(), lastModified); } //************************************************************************** //** getTabs //************************************************************************** /** Returns an html fragment used to render tabs. */ private String getTabs(String reqPath, Tabs tabs){ //Get tab entries LinkedHashMap<String, String> items = tabs.getItems(); Iterator<String> it = items.keySet().iterator(); String servletPath = getServletPath(); if (!servletPath.endsWith("/")) servletPath += "/"; //Create html fragment StringBuilder str = new StringBuilder(); while (it.hasNext()){ String text = it.next(); String link = items.get(text).replace("<%=Path%>", servletPath); boolean isActive = isActiveTab(text, link, reqPath); //System.out.println("|" + reqPath + "| vs |" + link + "|" + (isActive? " <--" : "")); str.append("<a href=\"" + link + "\">"); str.append("<div"); if (isActive) str.append(" class=\"active\""); str.append(">"); str.append(text); str.append("</div>"); str.append("</a>"); } return str.toString(); } //************************************************************************** //** isActiveTab //************************************************************************** /** Returns true if a given tab should be marked as active. * @param tabLabel Tab label as defined in tabs.txt * @param tabLink Tab URL as defined in tabs.txt * @param reqPath Relative path to the file on the server (relative to the web * directory). */ protected boolean isActiveTab(String tabLabel, String tabLink, String reqPath){ boolean isActive = false; if (reqPath.startsWith(tabLink)){ String servletPath = getServletPath(); if (!servletPath.endsWith("/")) servletPath += "/"; if (tabLink.equals(servletPath)){ isActive = reqPath.equals(servletPath); } else{ isActive = true; } } return isActive; } //************************************************************************** //** getBreadcrumbs //************************************************************************** /** Returns an html fragment used to render breadcrumb navigation links. * Breadcrumb navigation helps the user to understand their location in the * website by providing a breadcrumb trail back to the start page. */ protected String getBreadcrumbs(HttpServletRequest request){ StringBuilder str = new StringBuilder(); String path = request.getPath(); if (path.contains("?")) path = path.substring(0, path.indexOf("?")); if (!path.endsWith("/")) path += "/"; String servletPath = getServletPath(); if (!servletPath.endsWith("/")) servletPath += "/"; int idx = path.indexOf(servletPath); if (idx>-1){ String[] arr = path.substring(idx + servletPath.length()).split("/"); for (int i=0; i<arr.length; i++){ String text = arr[i].replace("_", " "); if (i<arr.length-1){ String link = servletPath + String.join("/", Arrays.copyOfRange(arr, 0, i+1)); str.append("<a href=\"" + link + "\">"); str.append("<div>"); str.append(text); str.append("</div>"); str.append("</a>"); } else{ str.append("<div>"); str.append(text); str.append("</div>"); } } } return str.toString(); } //************************************************************************** //** getSidebar //************************************************************************** /** Returns an html fragment used to render a sidebar. */ protected String getSidebar(HttpServletRequest request){ return ""; } //************************************************************************** //** getRedirect //************************************************************************** /** Returns true if a 301 response has been returned to the client. */ private boolean redirect(java.net.URL url, HttpServletResponse response) throws ServletException, IOException { String redirect = redirects.getRedirect(url); if (redirect!=null){ response.sendRedirect(redirect, true); return true; } return false; } } |