package org.jbake.template; import org.jbake.app.ContentStore; import org.jbake.model.DocumentTypeUtils; import org.jbake.template.model.PublishedCustomExtractor; import org.jbake.template.model.TypedDocumentsExtractor; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; import java.net.URL; import java.util.Enumeration; import java.util.Map; import java.util.Properties; import java.util.Set; import java.util.TreeMap; /** * <p>A singleton class giving access to model extractors. Model extractors are loaded based on classpath. New * rendering may be registered either at runtime (not recommanded) or by putting a descriptor file on classpath * (recommanded).</p> * <p>The descriptor file must be found in <i>META-INF</i> directory and named * <i>org.jbake.template.ModelExtractors.properties</i>. The format of the file is easy:</p> * <code>org.jbake.template.model.AllPosts=all_posts<br> org.jbake.template.model.AllContent=all_content<br> </code> * <p>where the key is the class of the extractor (must implement {@link ModelExtractor} and the value is the key * by which values are to be accessed in model.</p> * <p> * This class loads the engines only if they are found on classpath. If not, the engine is not registered. This allows * JBake to support multiple rendering engines without the explicit need to have them on classpath. This is a better fit * for embedding. * </p> * * @author ndx * @author Cédric Champeau */ public class ModelExtractors { private static final String PROPERTIES = "META-INF/org.jbake.template.ModelExtractors.properties"; private final static Logger LOGGER = LoggerFactory.getLogger(ModelExtractors.class); private final Map<String, ModelExtractor> extractors; private static class Loader { private static final ModelExtractors INSTANCE = new ModelExtractors(); } public static ModelExtractors getInstance() { return Loader.INSTANCE; } private ModelExtractors() { extractors = new TreeMap<String, ModelExtractor>(); loadEngines(); } public void registerEngine(String key, ModelExtractor extractor) { ModelExtractor old = extractors.put(key, extractor); if (old != null) { LOGGER.warn("Registered a model extractor for key [.{}] but another one was already defined: {}", key, old); } } /** * This method is used to search for a specific class, telling if loading the engine would succeed. This is * typically used to avoid loading optional modules. * * @param engineClassName engine class, used both as a hint to find it and to create the engine itself. @return null if the engine is not available, an instance of the engine otherwise */ private static ModelExtractor tryLoadEngine(String engineClassName) { try { @SuppressWarnings("unchecked") Class<? extends ModelExtractor> engineClass = (Class<? extends ModelExtractor>) Class.forName(engineClassName, false, ModelExtractors.class.getClassLoader()); return engineClass.newInstance(); } catch (ClassNotFoundException e) { return null; } catch (InstantiationException e) { return null; } catch (IllegalAccessException e) { return null; } catch (NoClassDefFoundError e) { // a dependency of the engine may not be found on classpath return null; } } /** * This method is used internally to load markup engines. Markup engines are found using descriptor files on * classpath, so adding an engine is as easy as adding a jar on classpath with the descriptor file included. */ private void loadEngines() { try { ClassLoader cl = ModelExtractors.class.getClassLoader(); Enumeration<URL> resources = cl.getResources(PROPERTIES); while (resources.hasMoreElements()) { URL url = resources.nextElement(); Properties props = new Properties(); props.load(url.openStream()); for (Map.Entry<Object, Object> entry : props.entrySet()) { String className = (String) entry.getKey(); String[] extensions = ((String) entry.getValue()).split(","); loadAndRegisterEngine(className, extensions); } } } catch (IOException e) { e.printStackTrace(); } } private void loadAndRegisterEngine(String className, String... extensions) { ModelExtractor engine = tryLoadEngine(className); if (engine != null) { for (String extension : extensions) { registerEngine(extension, engine); } } } public <Type> Type extractAndTransform(ContentStore db, String key, Map map, TemplateEngineAdapter<Type> adapter) throws NoModelExtractorException { if (extractors.containsKey(key)) { Object extractedValue = extractors.get(key).get(db, map, key); return adapter.adapt(key, extractedValue); } else { throw new NoModelExtractorException("no model extractor for key \"" + key + "\""); } } /** * @see java.util.Map#containsKey(java.lang.Object) * @param key A key a {@link ModelExtractor} is registered with * @return true if key is registered */ public boolean containsKey(Object key) { return extractors.containsKey(key); } /** * @return A @{@link Set} of all known keys a @{@link ModelExtractor} is registered with * @see java.util.Map#keySet() */ public Set<String> keySet() { return extractors.keySet(); } public void registerExtractorsForCustomTypes(String docType) { String pluralizedDoctype = DocumentTypeUtils.pluralize(docType); if (!containsKey(pluralizedDoctype)) { LOGGER.info("register new extractors for document type: " + docType); registerEngine(pluralizedDoctype, new TypedDocumentsExtractor()); registerEngine("published_" + pluralizedDoctype, new PublishedCustomExtractor(docType)); } } }