package org.jbake.app; import org.apache.commons.configuration.CompositeConfiguration; import org.apache.commons.configuration.ConfigurationException; import org.apache.commons.io.FilenameUtils; import org.jbake.app.ConfigUtil.Keys; import org.jbake.model.DocumentAttributes; import org.jbake.model.DocumentTypes; import org.jbake.render.RenderingTool; import org.jbake.template.ModelExtractorsDocumentTypeListener; import org.jbake.template.RenderingException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.File; import java.util.*; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * All the baking happens in the Oven! * * @author Jonathan Bullock <a href="mailto:jonbullock@gmail.com">jonbullock@gmail.com</a> */ public class Oven { private final static Logger LOGGER = LoggerFactory.getLogger(Oven.class); private final static Pattern TEMPLATE_DOC_PATTERN = Pattern.compile("(?:template\\.)([a-zA-Z0-9-_]+)(?:\\.file)"); private CompositeConfiguration config; private File source; private File destination; private File templatesPath; private File contentsPath; private File assetsPath; private boolean isClearCache; private List<Throwable> errors = new LinkedList<Throwable>(); private int renderedCount = 0; /** * Delegate c'tor to prevent API break for the moment. * * @param source Project source directory * @param destination The destination folder * @param isClearCache Should the cache be cleaned * @throws ConfigurationException if configuration is not loaded correctly */ public Oven(final File source, final File destination, final boolean isClearCache) throws ConfigurationException { this(source, destination, ConfigUtil.load(source), isClearCache); } /** * Creates a new instance of the Oven with references to the source and destination folders. * * @param source Project source directory * @param destination The destination folder * @param config Project configuration * @param isClearCache Should the cache be cleaned */ public Oven(final File source, final File destination, final CompositeConfiguration config, final boolean isClearCache) { this.source = source; this.destination = destination; this.config = config; this.isClearCache = isClearCache; } public CompositeConfiguration getConfig() { return config; } // TODO: do we want to use this. Else, config could be final public void setConfig(final CompositeConfiguration config) { this.config = config; } private void ensureSource() { if (!FileUtil.isExistingFolder(source)) { throw new JBakeException("Error: Source folder must exist: " + source.getAbsolutePath()); } if (!source.canRead()) { throw new JBakeException("Error: Source folder is not readable: " + source.getAbsolutePath()); } } private void ensureDestination() { if (null == destination) { destination = new File(source, config.getString(Keys.DESTINATION_FOLDER)); } if (!destination.exists()) { destination.mkdirs(); } if (!destination.canWrite()) { throw new JBakeException("Error: Destination folder is not writable: " + destination.getAbsolutePath()); } } private File setupPathFromConfig(String key) { return new File(FilenameUtils.concat(source.getAbsolutePath(), config.getString(key))); } /** * Checks source path contains required sub-folders (i.e. templates) and setups up variables for them. * * @throws JBakeException If template or contents folder don't exist */ public void setupPaths() { ensureSource(); templatesPath = setupRequiredFolderFromConfig(Keys.TEMPLATE_FOLDER); contentsPath = setupRequiredFolderFromConfig(Keys.CONTENT_FOLDER); assetsPath = setupPathFromConfig(Keys.ASSET_FOLDER); if (!assetsPath.exists()) { LOGGER.warn("No asset folder was found!"); } ensureDestination(); } private File setupRequiredFolderFromConfig(final String key) { final File path = setupPathFromConfig(key); if (!FileUtil.isExistingFolder(path)) { throw new JBakeException("Error: Required folder cannot be found! Expected to find [" + key + "] at: " + path.getAbsolutePath()); } return path; } /** * All the good stuff happens in here... * */ public void bake() { final ContentStore db = DBUtil.createDataStore(config.getString(Keys.DB_STORE), config.getString(Keys.DB_PATH)); updateDocTypesFromConfiguration(); DBUtil.updateSchema(db); try { final long start = new Date().getTime(); LOGGER.info("Baking has started..."); clearCacheIfNeeded(db); // process source content Crawler crawler = new Crawler(db, source, config); crawler.crawl(contentsPath); LOGGER.info("Content detected:"); for (String docType : DocumentTypes.getDocumentTypes()) { long count = db.getDocumentCount(docType); if (count > 0) { LOGGER.info("Parsed {} files of type: {}", count, docType); } } Renderer renderer = new Renderer(db, destination, templatesPath, config); for(RenderingTool tool : ServiceLoader.load(RenderingTool.class)) { try { renderedCount += tool.render(renderer, db, destination, templatesPath, config); } catch(RenderingException e) { errors.add(e); } } // mark docs as rendered for (String docType : DocumentTypes.getDocumentTypes()) { db.markConentAsRendered(docType); } // copy assets Asset asset = new Asset(source, destination, config); asset.copy(assetsPath); errors.addAll(asset.getErrors()); LOGGER.info("Baking finished!"); long end = new Date().getTime(); LOGGER.info("Baked {} items in {}ms", renderedCount, end - start); if (errors.size() > 0) { LOGGER.error("Failed to bake {} item(s)!", errors.size()); } } finally { db.close(); db.shutdown(); } } /** * Iterates over the configuration, searching for keys like "template.index.file=..." * in order to register new document types. */ private void updateDocTypesFromConfiguration() { ModelExtractorsDocumentTypeListener listener = new ModelExtractorsDocumentTypeListener(); DocumentTypes.addListener(listener); Iterator<String> keyIterator = config.getKeys(); while (keyIterator.hasNext()) { String key = keyIterator.next(); Matcher matcher = TEMPLATE_DOC_PATTERN.matcher(key); if (matcher.find()) { DocumentTypes.addDocumentType(matcher.group(1)); } } } private void clearCacheIfNeeded(final ContentStore db) { boolean needed = isClearCache; if (!needed) { DocumentList docs = db.getSignaturesForTemplates(); String currentTemplatesSignature; try { currentTemplatesSignature = FileUtil.sha1(templatesPath); } catch (Exception e) { currentTemplatesSignature = ""; } if (!docs.isEmpty()) { String sha1 = (String) docs.get(0).get(String.valueOf(DocumentAttributes.SHA1)); needed = !sha1.equals(currentTemplatesSignature); if (needed) { db.updateSignatures(currentTemplatesSignature); } } else { // first computation of templates signature db.insertSignature(currentTemplatesSignature); needed = true; } } if (needed) { for (String docType : DocumentTypes.getDocumentTypes()) { try { db.deleteAllByDocType(docType); } catch (Exception e) { // maybe a non existing document type } } DBUtil.updateSchema(db); } } public List<Throwable> getErrors() { return new ArrayList<Throwable>(errors); } }