/* This file is part of leafdigital picstory. picstory 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. picstory 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 picstory. If not, see <http://www.gnu.org/licenses/>. Copyright 2010 Samuel Marshall. */ package com.leafdigital.picstory; import java.io.*; import java.nio.charset.Charset; import java.text.SimpleDateFormat; import java.util.*; import javax.servlet.http.HttpServletResponse; import org.w3c.dom.*; /** * Handler for the index page (list of stories) */ public class IndexHandler extends RequestHandler { private final static long CACHE_EXPIRY = 1L * 60L * 1000L; private File cacheRoot, storyRoot; private XmlProcessors xml; private Cache cache; /** * Information held about a single folder. */ private class Folder implements Comparable<Folder> { private long updated, date; private String folder, error, title, thumbnailUrl; private Element description; private int thumbnailWidth, thumbnailHeight; /** * Constructs an indicator that there's an error with this folder. * @param folder Folder name * @param updated Updated date (from file) * @param error Error text (plain text) */ Folder(String folder, long updated, String error) { this.folder = folder; this.updated = updated; this.error = error; } /** * Constructs with all data. * @param folder Folder name * @param updated Updated date (from file) * @param date Specified date (from file), 0 if unknown * @param title Title of picstory * @param description Description of picstory * @param thumbnailUrl URL of thumbnail picture for picstory * @param thumbnailWidth Width of thumbnail picture * @param thumbnailHeight Height of thumbnail picture */ Folder(String folder, long updated, long date, String title, Element description, String thumbnailUrl, int thumbnailWidth, int thumbnailHeight) { this.folder = folder; this.updated = updated; this.date = date; this.title = title; this.description = description; this.thumbnailUrl = thumbnailUrl; this.thumbnailWidth = thumbnailWidth; this.thumbnailHeight = thumbnailHeight; } /** * Adds this folder to an XML element. * @param root Element to receive folder child. */ void add(Element root) { Document d = root.getOwnerDocument(); Element folderEl = d.createElement("folder"); root.appendChild(folderEl); folderEl.setAttribute("folder", folder); SimpleDateFormat format = Story.newDateFormat(); folderEl.setAttribute("updated", format.format(new Date(updated))); if(title != null) { folderEl.setAttribute("title", title); } if(description != null) { folderEl.appendChild(d.importNode(description, true)); } if(date != 0) { folderEl.setAttribute("date", format.format(new Date(date))); } if(thumbnailUrl != null) { folderEl.setAttribute("thumbnailUrl", thumbnailUrl); folderEl.setAttribute("thumbnailWidth", thumbnailWidth + ""); folderEl.setAttribute("thumbnailHeight", thumbnailHeight + ""); } if(error != null) { Element errorEl = d.createElement("error"); folderEl.appendChild(errorEl); errorEl.appendChild(d.createTextNode(error)); } } @Override public int hashCode() { return folder.hashCode(); } @Override public boolean equals(Object obj) { return (obj != null) && (obj instanceof Folder) && (((Folder)obj).folder.equals(folder)); } @Override public int compareTo(Folder o) { // Use date comparison first long thisDate = date == 0 ? updated : date; long otherDate = o.date == 0 ? o.updated : o.date; if(otherDate < thisDate) { return -1; } else if(otherDate > thisDate) { return 1; } else { return folder.compareTo(o.folder); } } } private class Cache { private long loadedDate; private String xhtml; /** * Loads cache. * @param reload True if cache should be reloaded * @param r Request is used to send data if the process will take a while * @throws InternalException Any error * @throws IOException I/O error */ private Cache(boolean reload, Request r) throws InternalException, IOException { File file = getCacheFile(); long cacheDate = file.lastModified(); try { boolean justMade = false; if(cacheDate == 0 || reload) { makeCacheFile(r); justMade = true; } // Load cache Document cache = xml.parseFile(file); // See if it's out of date if(!justMade) { // Check all folders File[] folderFiles = getFolderFiles(); long lastModified = 0; for(File folderFile : folderFiles) { if(!folderFile.isDirectory()) { continue; } File index = new File(folderFile, "index.xml"); long modified = index.lastModified(); lastModified = Math.max(lastModified, modified); if(lastModified > cacheDate) { break; } } // If necessary, re-make file if(lastModified > cacheDate) { makeCacheFile(r); cache = xml.parseFile(file); } } String xsl = getMainServlet().getTemplates().get( TemplateManager.Name.INDEX_XSL).getString(); Document xslDocument = xml.parseString( TemplateManager.Name.INDEX_XSL.getFilename(), xsl); xhtml = xml.transform(xslDocument, cache).replace("%%SITENAME%%", Util.esc(getMainServlet().getSiteName())).replace("%%INDEXINTRO%%", getMainServlet().getIndexIntroXhtml()).replace("%%INDEXFINAL%%", getMainServlet().getIndexFinalXhtml()); loadedDate = System.currentTimeMillis(); } catch(InternalException e) { xhtml = e.getErrorXhtml(r, getMainServlet()); loadedDate = System.currentTimeMillis(); } } private File getCacheFile() { return new File(cacheRoot, "index.cache"); } private void makeCacheFile(Request r) throws InternalException, IOException { // Set up request PrintWriter writer = startProgress(r); try { // Prepare XML document Document cache = xml.newDocument(); Element root = cache.createElement("index"); cache.appendChild(root); // Get all folders File[] folderFiles = getFolderFiles(); // Analyse all index.xml files Collection<Folder> folderSet = new TreeSet<Folder>(); for(File folderFile : folderFiles) { if(!folderFile.isDirectory()) { continue; } File index = new File(folderFile, "index.xml"); if(!index.exists()) { continue; } Folder result; String storyName = folderFile.getName(); try { updateProgress(writer, storyName); // Load story Story story = getMainServlet().getStories().getStory( storyName, false); // Get picture details Pic indexPic = story.getIndexPic(); String picUrl = storyName + "/" + indexPic.getFilename() + "." + indexPic.getHash() + ".w100.jpg"; // Create folder object result = new Folder(storyName, story.getLastModified(), story.getDate(), story.getTitle(), story.getDescription(), picUrl, indexPic.getWidth(), indexPic.getHeight()); } catch(Exception e) { // Create folder object with error message result = new Folder(storyName, index.lastModified(), e.getMessage()); } folderSet.add(result); } // Add all folders to cache xml file for(Folder folder : folderSet) { folder.add(root); } // Save cache file FileOutputStream output = new FileOutputStream(getCacheFile()); output.write(xml.saveString(cache).getBytes(Charset.forName("UTF-8"))); output.close(); finishProgress(writer, null); } catch(Throwable t) { finishProgress(writer, t); } } /** * @return All folders within storyRoot (empty array if none) */ private File[] getFolderFiles() { File[] folderFiles = storyRoot.listFiles(); if(folderFiles == null) { folderFiles = new File[0]; } return folderFiles; } /** * @return Date at which cache was last checked */ public long getLoadedDate() { return loadedDate; } /** * @return XHTML content of page */ public String getXhtml() { return xhtml; } } /** * @param mainServlet Main servlet * @param cacheRoot Root folder for cache * @param storyRoot Root folder for stories * @throws InternalException Any error constructing standard objects */ public IndexHandler(MainServlet mainServlet, File cacheRoot, File storyRoot) throws InternalException { super(mainServlet); this.cacheRoot = cacheRoot; this.storyRoot = storyRoot; xml = new XmlProcessors(); } /** * @param r HTTP request * @throws UserException User error or internal error * @throws IOException Any I/O error */ public void get(Request r) throws UserException, IOException { long now = System.currentTimeMillis(); synchronized(this) { if(cache == null || cache.getLoadedDate() + CACHE_EXPIRY < now || r.isReload()) { cache = new Cache(r.isReload(), r); if(r.sentData()) { return; } } } getMainServlet().sendPage(r, "index", null, cache.getXhtml()); } private PrintWriter startProgress(Request r) throws IOException, InternalException { PrintWriter writer = r.outputXhtmlHeaders(HttpServletResponse.SC_OK); Template template = getMainServlet().getTemplates().get( TemplateManager.Name.PROGRESS_START); writer.print( template.getString("", new String[] { "SITENAME" }, new String[] { Util.esc(getMainServlet().getSiteName()) })); return writer; } private void updateProgress(PrintWriter writer, String storyName) throws IOException, InternalException { Template template = getMainServlet().getTemplates().get( TemplateManager.Name.PROGRESS_UPDATE); writer.println( template.getString("", new String[] { "STORYNAME" }, new String[] { Util.esc(storyName) })); for(int i=0; i<20; i++) { writer.print(" "); } writer.println(); writer.flush(); } private void finishProgress(PrintWriter writer, Throwable t) throws IOException, InternalException { if(t == null) { Template template = getMainServlet().getTemplates().get( TemplateManager.Name.PROGRESS_FINISH); writer.print(template.getString()); } else { Template template = getMainServlet().getTemplates().get( TemplateManager.Name.PROGRESS_ERROR); writer.print( template.getString("", new String[] { "ERROR", "TRACE" }, new String[] { Util.esc(t.getMessage()), Util.esc(UserException.getTrace(t)) })); } writer.close(); } }