package org.jbake.app; import org.apache.commons.configuration.CompositeConfiguration; import org.jbake.app.ConfigUtil.Keys; import org.jbake.app.Crawler.Attributes; import org.jbake.template.DelegatingTemplateEngine; import org.jbake.util.PagingHelper; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.*; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; /** * Render output to a file. * * @author Jonathan Bullock <a href="mailto:jonbullock@gmail.com">jonbullock@gmail.com</a> */ public class Renderer { private interface RenderingConfig { File getPath(); String getName(); String getTemplate(); Map<String, Object> getModel(); } private static abstract class AbstractRenderingConfig implements RenderingConfig { protected final File path; protected final String name; protected final String template; public AbstractRenderingConfig(File path, String name, String template) { super(); this.path = path; this.name = name; this.template = template; } @Override public File getPath() { return path; } @Override public String getName() { return name; } @Override public String getTemplate() { return template; } } public class ModelRenderingConfig extends AbstractRenderingConfig { private final Map<String, Object> model; public ModelRenderingConfig(String fileName, Map<String, Object> model, String templateType) { super(new File(destination.getPath() + File.separator + fileName), fileName, findTemplateName(templateType)); this.model = model; } public ModelRenderingConfig(File path, String name, Map<String, Object> model, String template) { super(path, name, template); this.model = model; } @Override public Map<String, Object> getModel() { return model; } } class DefaultRenderingConfig extends AbstractRenderingConfig { private final Object content; private DefaultRenderingConfig(File path, String allInOneName) { super(path, allInOneName, findTemplateName(allInOneName)); this.content = buildSimpleModel(allInOneName); } public DefaultRenderingConfig(String filename, String allInOneName) { super(new File(destination.getPath() + File.separator + filename), allInOneName, findTemplateName(allInOneName)); this.content = buildSimpleModel(allInOneName); } /** * Constructor added due to known use of a allInOneName which is used for name, template and content * * @param allInOneName */ public DefaultRenderingConfig(String allInOneName) { this(new File(destination.getPath() + File.separator + allInOneName + config.getString(Keys.OUTPUT_EXTENSION)), allInOneName); } @Override public Map<String, Object> getModel() { Map<String, Object> model = new HashMap<String, Object>(); model.put("renderer", renderingEngine); model.put("content", content); if ( config.containsKey(Keys.PAGINATE_INDEX) && config.getBoolean(Keys.PAGINATE_INDEX) ) { model.put("numberOfPages", 0); model.put("currentPageNumber", 0); model.put("previousFileName", ""); model.put("nextFileName", ""); } return model; } } private final static Logger LOGGER = LoggerFactory.getLogger(Renderer.class); // TODO: should all content be made available to all templates via this class?? private final File destination; private final CompositeConfiguration config; private final DelegatingTemplateEngine renderingEngine; private final ContentStore db; /** * Creates a new instance of Renderer with supplied references to folders. * * @param db The database holding the content * @param destination The destination folder * @param templatesPath The templates folder * @param config Project configuration */ public Renderer(ContentStore db, File destination, File templatesPath, CompositeConfiguration config) { this.destination = destination; this.config = config; this.renderingEngine = new DelegatingTemplateEngine(config, db, destination, templatesPath); this.db = db; } /** * Creates a new instance of Renderer with supplied references to folders and the instance of DelegatingTemplateEngine to use. * * @param db The database holding the content * @param destination The destination folder * @param templatesPath The templates folder * @param config Project configuration * @param renderingEngine The instance of DelegatingTemplateEngine to use */ public Renderer(ContentStore db, File destination, File templatesPath, CompositeConfiguration config, DelegatingTemplateEngine renderingEngine) { this.destination = destination; this.config = config; this.renderingEngine = renderingEngine; this.db = db; } private String findTemplateName(String docType) { String templateKey = "template." + docType + ".file"; String returned = config.getString(templateKey); return returned; } /** * Render the supplied content to a file. * * @param content The content to renderDocument * @throws Exception if IOException or SecurityException are raised */ public void render(Map<String, Object> content) throws Exception { String docType = (String) content.get(Crawler.Attributes.TYPE); String outputFilename = destination.getPath() + File.separatorChar + content.get(Attributes.URI); if (outputFilename.lastIndexOf(".") > outputFilename.lastIndexOf(File.separatorChar)) { outputFilename = outputFilename.substring(0, outputFilename.lastIndexOf(".")); } // delete existing versions if they exist in case status has changed either way File draftFile = new File(outputFilename + config.getString(Keys.DRAFT_SUFFIX) + FileUtil.findExtension(config, docType)); if (draftFile.exists()) { draftFile.delete(); } File publishedFile = new File(outputFilename + FileUtil.findExtension(config, docType)); if (publishedFile.exists()) { publishedFile.delete(); } if (content.get(Crawler.Attributes.STATUS).equals(Crawler.Attributes.Status.DRAFT)) { outputFilename = outputFilename + config.getString(Keys.DRAFT_SUFFIX); } File outputFile = new File(outputFilename + FileUtil.findExtension(config, docType)); StringBuilder sb = new StringBuilder(); sb.append("Rendering [").append(outputFile).append("]... "); Map<String, Object> model = new HashMap<String, Object>(); model.put("content", content); model.put("renderer", renderingEngine); try { Writer out = createWriter(outputFile); renderingEngine.renderDocument(model, findTemplateName(docType), out); out.close(); sb.append("done!"); LOGGER.info(sb.toString()); } catch (Exception e) { sb.append("failed!"); LOGGER.error(sb.toString(), e); throw new Exception("Failed to render file " + outputFile.getAbsolutePath() + ". Cause: " + e.getMessage(), e); } } private Writer createWriter(File file) throws IOException { if (!file.exists()) { file.getParentFile().mkdirs(); file.createNewFile(); } return new OutputStreamWriter(new FileOutputStream(file), config.getString(ConfigUtil.Keys.RENDER_ENCODING)); } private void render(RenderingConfig renderConfig) throws Exception { File outputFile = renderConfig.getPath(); StringBuilder sb = new StringBuilder(); sb.append("Rendering ").append(renderConfig.getName()).append(" [").append(outputFile).append("]..."); try { Writer out = createWriter(outputFile); renderingEngine.renderDocument(renderConfig.getModel(), renderConfig.getTemplate(), out); out.close(); sb.append("done!"); LOGGER.info(sb.toString()); } catch (Exception e) { sb.append("failed!"); LOGGER.error(sb.toString(), e); throw new Exception("Failed to render " + renderConfig.getName(), e); } } /** * Render an index file using the supplied content. * * @param indexFile The name of the output file * @throws Exception if IOException or SecurityException are raised */ public void renderIndex(String indexFile) throws Exception { render(new DefaultRenderingConfig(indexFile, "masterindex")); } public void renderIndexPaging(String indexFile) throws Exception { long totalPosts = db.getPublishedCount("post"); int postsPerPage = config.getInt(Keys.POSTS_PER_PAGE, 5); if (totalPosts == 0) { //paging makes no sense. render single index file instead renderIndex(indexFile); } else { PagingHelper pagingHelper = new PagingHelper(totalPosts, postsPerPage); Map<String, Object> model = new HashMap<String, Object>(); model.put("renderer", renderingEngine); model.put("content", buildSimpleModel("masterindex")); model.put("numberOfPages", pagingHelper.getNumberOfPages()); try { db.setLimit(postsPerPage); for (int pageStart = 0, page = 1; pageStart < totalPosts; pageStart += postsPerPage, page++) { String fileName = indexFile; db.setStart(pageStart); model.put("currentPageNumber", page); String previous = pagingHelper.getPreviousFileName(page, fileName); model.put("previousFileName", previous); String nextFileName = pagingHelper.getNextFileName(page, fileName); model.put("nextFileName", nextFileName); // Add page number to file name fileName = pagingHelper.getCurrentFileName(page, fileName); ModelRenderingConfig renderConfig = new ModelRenderingConfig(fileName, model, "masterindex"); render(renderConfig); } db.resetPagination(); } catch (Exception e) { throw new Exception("Failed to render index. Cause: " + e.getMessage(), e); } } } /** * Render an XML sitemap file using the supplied content. * * @param sitemapFile configuration for site map * @throws Exception if can't create correct default rendering config * * @see <a href="https://support.google.com/webmasters/answer/156184?hl=en&ref_topic=8476">About Sitemaps</a> * @see <a href="http://www.sitemaps.org/">Sitemap protocol</a> */ public void renderSitemap(String sitemapFile) throws Exception { render(new DefaultRenderingConfig(sitemapFile, "sitemap")); } /** * Render an XML feed file using the supplied content. * * @param feedFile The name of the output file * @throws Exception if default rendering configuration is not loaded correctly */ public void renderFeed(String feedFile) throws Exception { render(new DefaultRenderingConfig(feedFile, "feed")); } /** * Render an archive file using the supplied content. * * @param archiveFile The name of the output file * @throws Exception if default rendering configuration is not loaded correctly */ public void renderArchive(String archiveFile) throws Exception { render(new DefaultRenderingConfig(archiveFile, "archive")); } /** * Render tag files using the supplied content. * * @param tagPath The output path * @return Number of rendered tags * @throws Exception if cannot render tags correctly */ public int renderTags(String tagPath) throws Exception { int renderedCount = 0; final List<Throwable> errors = new LinkedList<Throwable>(); for (String tag : db.getAllTags()) { try { Map<String, Object> model = new HashMap<String, Object>(); model.put("renderer", renderingEngine); model.put(Attributes.TAG, tag); Map<String, Object> map = buildSimpleModel(Attributes.TAG); map.put(Attributes.ROOTPATH, "../"); model.put("content", map); File path = new File(destination.getPath() + File.separator + tagPath + File.separator + tag + config.getString(Keys.OUTPUT_EXTENSION)); render(new ModelRenderingConfig(path, Attributes.TAG, model, findTemplateName(Attributes.TAG))); renderedCount++; } catch (Exception e) { errors.add(e); } } if (!errors.isEmpty()) { StringBuilder sb = new StringBuilder(); sb.append("Failed to render tags. Cause(s):"); for (Throwable error : errors) { sb.append("\n").append(error.getMessage()); } throw new Exception(sb.toString(), errors.get(0)); } else { return renderedCount; } } /** * Builds simple map of values, which are exposed when rendering index/archive/sitemap/feed/tags. * * @param type * @return */ private Map<String, Object> buildSimpleModel(String type) { Map<String, Object> content = new HashMap<String, Object>(); content.put(Attributes.TYPE, type); content.put(Attributes.ROOTPATH, ""); // add any more keys here that need to have a default value to prevent need to perform null check in templates return content; } }