package org.jbake.parser;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.net.URL;
import java.util.Arrays;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
/**
* <p>A singleton class giving access to markup engines. Markup engines are loaded based on classpath.
* New engines 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.parser.MarkupEngines.properties</i>. The format of the file is easy:</p>
* <code>
* org.jbake.parser.RawMarkupEngine=html<br>
* org.jbake.parser.AsciidoctorEngine=ad,adoc,asciidoc<br>
* org.jbake.parser.MarkdownEngine=md<br>
* </code>
* <p>where the key is the class of the engine (must extend {@link org.jbake.parser.MarkupEngine} and have a no-arg
* constructor and the value is a comma-separated list of file extensions that this engine is capable of proceeding.</p>
*
* <p>Markup engines are singletons, so are typically used to initialize the underlying renderning engines. They
* <b>must not</b> store specific information of a currently processed file (use {@link ParserContext the parser context}
* for that).</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.
*
* @author Cédric Champeau
*
*/
public class Engines {
private final static Logger LOGGER = LoggerFactory.getLogger(Engines.class);
private final static Engines INSTANCE;
static {
INSTANCE = new Engines();
loadEngines();
}
private final Map<String, ParserEngine> parsers;
public static ParserEngine get(String fileExtension) {
return INSTANCE.getEngine(fileExtension);
}
public static void register(String fileExtension, ParserEngine engine) {
INSTANCE.registerEngine(fileExtension, engine);
}
public static Set<String> getRecognizedExtensions() {
return Collections.unmodifiableSet(INSTANCE.parsers.keySet());
}
private Engines() {
parsers = new HashMap<String, ParserEngine>();
}
private void registerEngine(String fileExtension, ParserEngine markupEngine) {
ParserEngine old = parsers.put(fileExtension, markupEngine);
if (old != null) {
LOGGER.warn("Registered a markup engine for extension [.{}] but another one was already defined: {}", fileExtension, old);
}
}
private ParserEngine getEngine(String fileExtension) {
return parsers.get(fileExtension);
}
/**
* 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 ParserEngine tryLoadEngine(String engineClassName) {
try {
@SuppressWarnings("unchecked")
Class<? extends ParserEngine> engineClass = (Class<? extends ParserEngine>) Class.forName(engineClassName, false, Engines.class.getClassLoader());
return engineClass.newInstance();
} catch (ClassNotFoundException e) {
return new ErrorEngine(engineClassName);
} catch (InstantiationException e) {
return new ErrorEngine(engineClassName);
} catch (IllegalAccessException e) {
return new ErrorEngine(engineClassName);
} catch (NoClassDefFoundError e) {
// a dependency of the engine may not be found on classpath
return new ErrorEngine(engineClassName);
}
}
/**
* 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 static void loadEngines() {
try {
ClassLoader cl = Engines.class.getClassLoader();
Enumeration<URL> resources = cl.getResources("META-INF/org.jbake.parser.MarkupEngines.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(",");
registerEngine(className, extensions);
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
private static void registerEngine(String className, String... extensions) {
ParserEngine engine = tryLoadEngine(className);
if (engine != null) {
for (String extension : extensions) {
register(extension, engine);
}
if (engine instanceof ErrorEngine) {
LOGGER.warn("Unable to load a suitable rendering engine for extensions {}", Arrays.toString(extensions));
}
}
}
}