Java tutorial
/* * Copyright (C) 2012 McEvoy Software Ltd * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * 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 General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package io.milton.cloud.server.web.templating; import io.milton.cloud.server.apps.orgs.OrganisationRootFolder; import io.milton.cloud.server.apps.website.WebsiteRootFolder; import io.milton.common.Path; import io.milton.resource.Resource; import org.apache.velocity.Template; import org.apache.velocity.app.VelocityEngine; import java.io.*; import java.net.URL; import java.util.*; import org.apache.commons.collections.ExtendedProperties; import org.apache.velocity.exception.ResourceNotFoundException; import org.apache.velocity.runtime.resource.loader.ResourceLoader; import io.milton.cloud.server.apps.ApplicationManager; import io.milton.cloud.server.web.*; import io.milton.context.RequestContext; import static io.milton.context.RequestContext._; import io.milton.http.exceptions.BadRequestException; import io.milton.http.exceptions.NotAuthorizedException; import io.milton.http.exceptions.NotFoundException; import javax.servlet.ServletContext; import javax.xml.stream.XMLStreamException; /** * Builds pages by plugging a few things in together: - a static skeleton for a * HTML page, ie the html, header and body tags - a Theme, which defines web * resources, such as js and css files, to include in the header. And which * defines one or more layouts for the body. - the template itself, which * provides more web resources to be included in the header, and the body layout * which will be injected into the theme template. Also defines a parameter * indicating which theme template to use * * @author brad */ public class HtmlTemplater { private static org.apache.log4j.Logger log = org.apache.log4j.Logger.getLogger(HtmlTemplater.class); public static final String ROOTS_SYS_PROP_NAME = "template.file.roots"; private List<File> roots; private final ServletContext servletContext; private final VelocityEngine engine; private final SpliffySecurityManager securityManager; private final HtmlTemplateLoader templateLoader; private final HtmlTemplateParser templateParser; private final HtmlTemplateRenderer templateRenderer; private final Map<String, TemplateHtmlPage> cachedTemplateMetaData = new HashMap<>(); private String defaultPublicTheme = "fuse"; private String defaultAdminTheme = "admin"; private Path webRoot = Path.path("/"); public HtmlTemplater(ApplicationManager applicationManager, Formatter formatter, SpliffySecurityManager securityManager, ServletContext servletContext) { this.servletContext = servletContext; this.securityManager = securityManager; templateLoader = new HtmlTemplateLoader(); java.util.Properties p = new java.util.Properties(); p.setProperty("runtime.log.logsystem.class", "org.apache.velocity.runtime.log.NullLogSystem"); engine = new VelocityEngine(p); engine.setProperty("resource.loader", "mine"); engine.setProperty("mine.resource.loader.instance", new HtmlTemplateLoaderResourceLoader()); engine.setProperty("userdirective", VelocityContentDirective.class.getName() + "," + PortletsDirective.class.getName() + "," + RenderAppSettingsDirective.class.getName()); templateParser = new HtmlTemplateParser(); templateRenderer = new HtmlTemplateRenderer(applicationManager, formatter); roots = new ArrayList<>(); File fWebappRoot = new File(servletContext.getRealPath("/")); roots.add(fWebappRoot); log.info("Using webapp root dir: " + fWebappRoot.getAbsolutePath()); String extraRoots = System.getProperty(ROOTS_SYS_PROP_NAME); if (extraRoots != null && !extraRoots.isEmpty()) { String[] arr = extraRoots.split(","); for (String s : arr) { File root = new File(s); if (!root.exists()) { throw new RuntimeException("Root template dir specified in system property does not exist: " + root.getAbsolutePath() + " from property value: " + extraRoots); } roots.add(root); log.info("Using file template root: " + root.getAbsolutePath()); } } } /** * Tries to find if the page isPublic by checkign to see if it implements * CommonResource, and finds an appropriate theme if possible from the root * folder * * @param templatePath * @param aThis * @param params * @param out * @throws IOException */ public void writePage(String templatePath, Resource aThis, Map<String, String> params, OutputStream out) throws IOException { boolean isPublic = false; if (aThis instanceof CommonResource) { CommonResource cr = (CommonResource) aThis; isPublic = cr.isPublic(); } String theme = findTheme(aThis, isPublic); writePage(theme, templatePath, aThis, params, out); } /** * Finds an appropriate theme by looking up the root folder to find if there * is a website, and if so it uses the isPublic flag to decide if to use the * public or internal theme from the website. */ public void writePage(boolean isPublic, String templatePath, Resource aThis, Map<String, String> params, OutputStream out) throws IOException { String theme = findTheme(aThis, isPublic); writePage(theme, templatePath, aThis, params, out); } /** * Generate a templated page with the given theme and template. The theme * controls the "chrome", ie the menu, header, footer, etc. The template * controls the layout of this specific page, ie contacts, calendar, file * list, etc * * @param theme * @param templatePath * @param aThis * @param params * @param out * @throws IOException */ public void writePage(String theme, String templatePath, Resource aThis, Map<String, String> params, OutputStream out) throws IOException { RootFolder rootFolder = WebUtils.findRootFolder(aThis); RequestContext.getCurrent().put(rootFolder); if (theme.equals("custom")) { RequestContext.getCurrent().put("isCustom", Boolean.TRUE); // can't pass params to velocity template loader, so have to workaround } String themePath; // path that contains the theme templates Eg /templates/themes/admin/ or /content/theme/ if (theme.equals("custom")) { themePath = "/content/theme/"; } else { themePath = "/templates/themes/" + theme + "/"; } UserResource user = securityManager.getCurrentPrincipal(); if (!templatePath.startsWith("/")) { if (templatePath.startsWith("theme/")) { templatePath = templatePath.replace("theme/", themePath); // Eg change theme/page to /content/theme/page System.out.println("writePage: replaced theme => " + templatePath); } else { templatePath = "/templates/apps/" + templatePath; // Eg change admin/manageUsers to /templates/apps/admin/manageUsers } } if (!templatePath.endsWith(".html")) { templatePath = templatePath + ".html"; } Template bodyTemplate = getTemplate(templatePath); TemplateHtmlPage bodyTemplateMeta = cachedTemplateMetaData.get(templatePath); String themeTemplateName = findThemeTemplateName(bodyTemplateMeta); String themeTemplatePath; // if the given themeTemplateName is an absolute path then use it as is, other prefix with themePath if (themeTemplateName.startsWith("/")) { themeTemplatePath = themeTemplateName; } else { themeTemplatePath = themePath + themeTemplateName; } if (!themeTemplatePath.endsWith(".html")) { themeTemplatePath += ".html"; // this class only does html templates } Template themeTemplate = getTemplate(themeTemplatePath); if (themeTemplate == null) { throw new RuntimeException("Couldnt find themeTemplate: " + themeTemplatePath); } TemplateHtmlPage themeTemplateTemplateMeta = cachedTemplateMetaData.get(themeTemplatePath); if (themeTemplateTemplateMeta == null) { throw new RuntimeException("Couldnt find meta for template: " + themeTemplatePath); } templateRenderer.renderHtml(rootFolder, aThis, params, user, themeTemplate, themeTemplateTemplateMeta, bodyTemplate, bodyTemplateMeta, theme, out); } public String findTheme(Resource r, boolean isPublic) { RootFolder rootFolder = WebUtils.findRootFolder(r); String theme; if (rootFolder instanceof OrganisationRootFolder) { theme = defaultAdminTheme; } else if (rootFolder instanceof WebsiteRootFolder) { WebsiteRootFolder websiteFolder = (WebsiteRootFolder) rootFolder; if (isPublic) { theme = websiteFolder.getWebsite().getPublicTheme(); } else { theme = websiteFolder.getWebsite().getInternalTheme(); } if (theme == null) { theme = defaultPublicTheme; } } else { theme = defaultPublicTheme; } return theme; } private String findThemeTemplateName(TemplateHtmlPage bodyTemplateMeta) { for (WebResource wr : bodyTemplateMeta.getWebResources()) { if (wr.getTag().equals("link")) { String rel = wr.getAtts().get("rel"); if (rel != null && rel.equals("template")) { String templateName = wr.getAtts().get("href"); return templateName; } } } return "normal"; } public String getDefaultTheme() { return defaultPublicTheme; } public void setDefaultTheme(String defaultTheme) { this.defaultPublicTheme = defaultTheme; } public String getWebRoot() { return webRoot.toString(); } public void setWebRoot(String webRoot) { this.webRoot = Path.path(webRoot); } public List<File> getTemplateFileRoots() { return roots; } public void setTemplateFileRoots(List<File> roots) { this.roots = roots; } private Template getTemplate(String templatePath) { if (templatePath == null) { throw new RuntimeException("templatePath is null"); } return engine.getTemplate(templatePath); } public class HtmlTemplateLoaderResourceLoader extends ResourceLoader { @Override public void init(ExtendedProperties configuration) { } @Override public InputStream getResourceStream(String source) throws ResourceNotFoundException { TemplateHtmlPage meta; try { meta = templateLoader.findTemplateSource(source); } catch (Exception ex) { System.out.println("Exception loading template"); ex.printStackTrace(); log.error("exception loading template: " + source, ex); return null; } if (meta == null) { throw new ResourceNotFoundException(source); } return new ByteArrayInputStream(meta.getBody().getBytes()); } @Override public boolean isSourceModified(org.apache.velocity.runtime.resource.Resource resource) { TemplateHtmlPage meta; try { meta = templateLoader.findTemplateSource(resource.getName()); return meta.getTimestamp() != resource.getLastModified(); } catch (IOException ex) { throw new RuntimeException(ex); } } @Override public long getLastModified(org.apache.velocity.runtime.resource.Resource resource) { TemplateHtmlPage meta; try { meta = templateLoader.findTemplateSource(resource.getName()); return meta.getTimestamp(); } catch (IOException ex) { throw new RuntimeException(ex); } } } public String getDefaultAdminTheme() { return defaultAdminTheme; } public void setDefaultAdminTheme(String defaultAdminTheme) { this.defaultAdminTheme = defaultAdminTheme; } public String getDefaultPublicTheme() { return defaultPublicTheme; } public void setDefaultPublicTheme(String defaultPublicTheme) { this.defaultPublicTheme = defaultPublicTheme; } public class HtmlTemplateLoader { /** * We'll look for the template in the website's repository (if using * custom theme), then in the webapp filesystem, then in the regular * filesystem, then finally in the classpath */ public TemplateHtmlPage findTemplateSource(String path) throws IOException { log.info("findTemplateSource: " + path); TemplateHtmlPage meta = cachedTemplateMetaData.get(path); if (meta != null) { if (!meta.isValid()) { cachedTemplateMetaData.remove(path); meta = null; } else { log.info("cache hit: " + meta.getSource() + " - " + meta.getClass()); return meta; } } long tm = System.currentTimeMillis(); Boolean isCustom = RequestContext.getCurrent().get("isCustom"); if (isCustom == null) { isCustom = false; } Path p = Path.path(path); Path webPath = webRoot.add(p).getParent(); // go to parent, because the path is the directory which contains the template if (isCustom) { // load from website repo RootFolder rootFolder = _(RootFolder.class); WebsiteRootFolder wrf = (WebsiteRootFolder) rootFolder; try { Resource r = NodeChildUtils.find(p, wrf); FileResource fr = NodeChildUtils.toFileResource(r); if (fr == null) { if (!"VM_global_library.vm".equals(path)) { log.info("Couldnt find template: " + path + " in website: " + wrf.getWebsite().getDomainName()); } } else { meta = loadContentMeta(fr, wrf.getWebsite().getDomainName(), webPath); } } catch (NotAuthorizedException | BadRequestException | NotFoundException ex) { throw new IOException(ex); } } if (meta == null && roots != null) { for (File root : roots) { File templateFile = new File(root, path); if (!templateFile.exists()) { log.warn("Template does not exist: " + templateFile.getAbsolutePath()); } else { log.info("found file: " + templateFile.getAbsolutePath()); meta = loadFileMeta(templateFile, webPath); break; } } } // Not in filesystem, try a servlet resource if (meta == null && path.startsWith("/")) { // try to locate a physical file path, so we can detect file changes String realPath = servletContext.getRealPath(path); if (realPath != null) { File templateFile = new File(realPath); if (templateFile.exists() && templateFile.isFile()) { meta = loadFileMeta(templateFile, webPath); } } // if we couldnt get a real path might be because its a WAR packaged resource, or overlay, so // try to get url. if (meta == null) { URL resource = servletContext.getResource(path); if (resource != null) { meta = loadClassPathMeta(resource, webPath); } } // String localWebPath = servletContext.getRealPath(path); // InputStream r = servletContext.getResourceAsStream(path); // System.out.println("res as str: " + r); // System.out.println("localwebpath: " + localWebPath); // if (localWebPath != null) { // File templateFile = new File(localWebPath); // System.out.println("f: " + templateFile.getCanonicalPath() + " - " + templateFile.exists()); // if (templateFile.exists()) { // log.info("found web resource: " + templateFile.getAbsolutePath()); // meta = loadFileMeta(templateFile, webPath); // } // } } // if not in filesystem, try classpath if (meta == null) { URL resource = this.getClass().getResource(path); if (resource != null) { meta = loadClassPathMeta(resource, webPath); } } if (meta != null) { tm = System.currentTimeMillis() - tm; log.info("cache miss: " + meta.getSource() + " - " + meta.getClass() + " parsed in " + tm + "ms"); cachedTemplateMetaData.put(path, meta); } else { log.warn("Failed to find: " + path); } return meta; } public long getLastModified(Object o) { TemplateHtmlPage templateFile = (TemplateHtmlPage) o; return templateFile.getTimestamp(); } public Reader getReader(Object o, String path) throws IOException { TemplateHtmlPage meta = (TemplateHtmlPage) o; if (meta.getBody() == null) { throw new RuntimeException("No template text for: " + meta.getId()); } return new StringReader(meta.getBody()); } private TemplateHtmlPage loadClassPathMeta(URL resource, Path webPath) throws IOException { ClassPathTemplateHtmlPage meta = new ClassPathTemplateHtmlPage(resource); try { templateParser.parse(meta, webPath); // needs web path to evaluate resource paths in templates } catch (XMLStreamException ex) { throw new IOException(resource.toString(), ex); } return meta; } private TemplateHtmlPage loadFileMeta(File templateFile, Path webPath) throws IOException { FileTemplateHtmlPage meta = new FileTemplateHtmlPage(templateFile); try { templateParser.parse(meta, webPath); // needs web path to evaluate resource paths in templates } catch (XMLStreamException ex) { throw new IOException(templateFile.getCanonicalPath(), ex); } return meta; } private TemplateHtmlPage loadContentMeta(FileResource fr, String websiteName, Path webPath) throws IOException, NotAuthorizedException, BadRequestException, NotFoundException { ContentTemplateHtmlPage meta = new ContentTemplateHtmlPage(fr, websiteName); try { templateParser.parse(meta, webPath); // needs web path to evaluate resource paths in templates } catch (XMLStreamException ex) { throw new IOException(fr.getHref(), ex); } return meta; } } }