diff --git a/gradle.properties b/gradle.properties index 21b0e8f7..1d8892d2 100644 --- a/gradle.properties +++ b/gradle.properties @@ -28,6 +28,8 @@ pebbleVersion = 3.1.5 slf4jVersion = 1.7.30 snakeYamlVersion = 1.28 thymeleafVersion = 3.0.12.RELEASE +picocli = 4.6.1 + # testing dependencies junit4Version = 4.13.2 diff --git a/jbake-core/build.gradle b/jbake-core/build.gradle index 253ccd03..d4780c06 100644 --- a/jbake-core/build.gradle +++ b/jbake-core/build.gradle @@ -30,7 +30,7 @@ dependencies { // cli specific dependencies implementation "org.eclipse.jetty:jetty-server:$jettyServerVersion", optional - implementation "args4j:args4j:$args4jVersion", optional + implementation "info.picocli:picocli:$picocli", optional } processResources { diff --git a/jbake-core/src/main/java/org/jbake/app/ContentStore.java b/jbake-core/src/main/java/org/jbake/app/ContentStore.java index 5627ce6e..a5bb89fb 100644 --- a/jbake-core/src/main/java/org/jbake/app/ContentStore.java +++ b/jbake-core/src/main/java/org/jbake/app/ContentStore.java @@ -36,6 +36,7 @@ import com.orientechnologies.orient.core.sql.OCommandSQL; import com.orientechnologies.orient.core.sql.executor.OResultSet; import com.orientechnologies.orient.core.sql.query.OSQLSynchQuery; +import org.jbake.launcher.SystemExit; import org.jbake.model.DocumentAttributes; import org.jbake.model.DocumentTypes; import org.slf4j.Logger; @@ -212,7 +213,7 @@ public ODocument mergeDocument(Map incomingDocMap) { activateOnCurrentThread(); List results = db.command(new OSQLSynchQuery(sql)).execute(sourceUri); if (results.isEmpty()) { - throw new JBakeException("No document with sourceUri '" + sourceUri + "'."); + throw new JBakeException(SystemExit.ERROR, "No document with sourceUri '" + sourceUri + "'."); } // Update it from the given map. diff --git a/jbake-core/src/main/java/org/jbake/app/JBakeException.java b/jbake-core/src/main/java/org/jbake/app/JBakeException.java index a3af314e..f3e858a4 100644 --- a/jbake-core/src/main/java/org/jbake/app/JBakeException.java +++ b/jbake-core/src/main/java/org/jbake/app/JBakeException.java @@ -1,5 +1,7 @@ package org.jbake.app; +import org.jbake.launcher.SystemExit; + /** * This runtime exception is thrown by JBake API to indicate an processing * error. @@ -9,18 +11,26 @@ public class JBakeException extends RuntimeException { private static final long serialVersionUID = 1L; + final private SystemExit exit; + /** + * * @param message * The error message. * @param cause * The causing exception or null if no cause * available. */ - public JBakeException(final String message, final Throwable cause) { + public JBakeException(final SystemExit exit, final String message, final Throwable cause) { super(message, cause); + this.exit = exit; + } + + public JBakeException(final SystemExit exit, final String message) { + this(exit, message, null); } - public JBakeException(final String message) { - this(message, null); + public int getExit() { + return exit.getStatus(); } } diff --git a/jbake-core/src/main/java/org/jbake/app/configuration/ConfigUtil.java b/jbake-core/src/main/java/org/jbake/app/configuration/ConfigUtil.java index 08bcf029..c55befaf 100644 --- a/jbake-core/src/main/java/org/jbake/app/configuration/ConfigUtil.java +++ b/jbake-core/src/main/java/org/jbake/app/configuration/ConfigUtil.java @@ -5,14 +5,12 @@ import org.apache.commons.configuration.PropertiesConfiguration; import org.apache.commons.configuration.SystemConfiguration; import org.jbake.app.JBakeException; +import org.jbake.launcher.SystemExit; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.File; import java.net.URL; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.nio.charset.Charset; /** * Provides Configuration related functions. @@ -29,10 +27,10 @@ public class ConfigUtil { private CompositeConfiguration load(File source) throws ConfigurationException { if (!source.exists()) { - throw new JBakeException("The given source folder '" + source.getAbsolutePath() + "' does not exist."); + throw new JBakeException(SystemExit.CONFIGURATION_ERROR, "The given source folder '" + source.getAbsolutePath() + "' does not exist."); } if (!source.isDirectory()) { - throw new JBakeException("The given source folder is not a directory."); + throw new JBakeException(SystemExit.CONFIGURATION_ERROR,"The given source folder is not a directory."); } CompositeConfiguration config = new CompositeConfiguration(); config.setListDelimiter(','); diff --git a/jbake-core/src/main/java/org/jbake/app/configuration/JBakeConfigurationInspector.java b/jbake-core/src/main/java/org/jbake/app/configuration/JBakeConfigurationInspector.java index 2fb214d9..11f24c8d 100644 --- a/jbake-core/src/main/java/org/jbake/app/configuration/JBakeConfigurationInspector.java +++ b/jbake-core/src/main/java/org/jbake/app/configuration/JBakeConfigurationInspector.java @@ -2,6 +2,7 @@ import org.jbake.app.FileUtil; import org.jbake.app.JBakeException; +import org.jbake.launcher.SystemExit; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -28,10 +29,10 @@ public void inspect() throws JBakeException { private void ensureSource() throws JBakeException { File source = configuration.getSourceFolder(); if (!FileUtil.isExistingFolder(source)) { - throw new JBakeException("Error: Source folder must exist: " + source.getAbsolutePath()); + throw new JBakeException(SystemExit.CONFIGURATION_ERROR, "Error: Source folder must exist: " + source.getAbsolutePath()); } if (!configuration.getSourceFolder().canRead()) { - throw new JBakeException("Error: Source folder is not readable: " + source.getAbsolutePath()); + throw new JBakeException(SystemExit.CONFIGURATION_ERROR, "Error: Source folder is not readable: " + source.getAbsolutePath()); } } @@ -51,7 +52,7 @@ private void ensureDestination() { destination.mkdirs(); } if (!destination.canWrite()) { - throw new JBakeException("Error: Destination folder is not writable: " + destination.getAbsolutePath()); + throw new JBakeException(SystemExit.CONFIGURATION_ERROR, "Error: Destination folder is not writable: " + destination.getAbsolutePath()); } } @@ -62,9 +63,9 @@ private void checkAssetFolder() { } } - private void checkRequiredFolderExists(String property, File path) { + private void checkRequiredFolderExists(String folderName, File path) { if (!FileUtil.isExistingFolder(path)) { - throw new JBakeException("Error: Required folder cannot be found! Expected to find [" + property + "] at: " + path.getAbsolutePath()); + throw new JBakeException(SystemExit.CONFIGURATION_ERROR, "Error: Required folder cannot be found! Expected to find [" + folderName + "] at: " + path.getAbsolutePath()); } } diff --git a/jbake-core/src/main/java/org/jbake/launcher/Baker.java b/jbake-core/src/main/java/org/jbake/launcher/Baker.java index a1dcb373..17d17f8f 100644 --- a/jbake-core/src/main/java/org/jbake/launcher/Baker.java +++ b/jbake-core/src/main/java/org/jbake/launcher/Baker.java @@ -41,7 +41,7 @@ public void bake(final JBakeConfiguration config) { msg.append(MessageFormat.format("{0}. {1}\n", errNr, error.getMessage())); ++errNr; } - throw new JBakeException(msg.toString(), errors.get(0)); + throw new JBakeException(SystemExit.ERROR ,msg.toString(), errors.get(0)); } } } diff --git a/jbake-core/src/main/java/org/jbake/launcher/JettyServer.java b/jbake-core/src/main/java/org/jbake/launcher/JettyServer.java index 79504568..cff10593 100644 --- a/jbake-core/src/main/java/org/jbake/launcher/JettyServer.java +++ b/jbake-core/src/main/java/org/jbake/launcher/JettyServer.java @@ -7,6 +7,7 @@ import org.eclipse.jetty.server.handler.DefaultHandler; import org.eclipse.jetty.server.handler.HandlerList; import org.eclipse.jetty.server.handler.ResourceHandler; +import org.jbake.app.JBakeException; import org.jbake.app.configuration.JBakeConfiguration; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -34,41 +35,42 @@ public void run(String resourceBase, String port) { public void run(String resourceBase, JBakeConfiguration configuration) { run(resourceBase, configuration.getServerContextPath(), configuration.getServerHostname(), configuration.getServerPort()); } + /** * Run Jetty web server serving out supplied path on supplied port * * @param resourceBase Base directory for resources to be served - * @param port Required server port + * @param port Required server port */ private void run(String resourceBase, String contextPath, String hostname, int port) { - server = new Server(); - ServerConnector connector = new ServerConnector(server); - connector.setHost(hostname); - connector.setPort(port); - server.addConnector(connector); + try { + server = new Server(); + ServerConnector connector = new ServerConnector(server); + connector.setHost(hostname); + connector.setPort(port); + server.addConnector(connector); - ResourceHandler resource_handler = new ResourceHandler(); - resource_handler.setDirectoriesListed(true); - resource_handler.setWelcomeFiles(new String[]{"index", "index.html"}); - resource_handler.setResourceBase(resourceBase); + ResourceHandler resource_handler = new ResourceHandler(); + resource_handler.setDirectoriesListed(true); + resource_handler.setWelcomeFiles(new String[]{"index", "index.html"}); + resource_handler.setResourceBase(resourceBase); - ContextHandler contextHandler = new ContextHandler(); - contextHandler.setContextPath(contextPath); - contextHandler.setHandler(resource_handler); + ContextHandler contextHandler = new ContextHandler(); + contextHandler.setContextPath(contextPath); + contextHandler.setHandler(resource_handler); - HandlerList handlers = new HandlerList(); + HandlerList handlers = new HandlerList(); - handlers.setHandlers(new Handler[]{contextHandler, new DefaultHandler()}); - server.setHandler(handlers); + handlers.setHandlers(new Handler[]{contextHandler, new DefaultHandler()}); + server.setHandler(handlers); - LOGGER.info("Serving out contents of: [{}] on http://{}:{}{}", resourceBase, hostname, port, contextHandler.getContextPath()); - LOGGER.info("(To stop server hit CTRL-C)"); + LOGGER.info("Serving out contents of: [{}] on http://{}:{}{}", resourceBase, hostname, port, contextHandler.getContextPath()); + LOGGER.info("(To stop server hit CTRL-C)"); - try { server.start(); server.join(); } catch (Exception e) { - LOGGER.error("unable to start server", e); + throw new JBakeException(SystemExit.SERVER_ERROR, "unable to start the server", e); } } diff --git a/jbake-core/src/main/java/org/jbake/launcher/LaunchOptions.java b/jbake-core/src/main/java/org/jbake/launcher/LaunchOptions.java index d30bd586..5f3b1914 100644 --- a/jbake-core/src/main/java/org/jbake/launcher/LaunchOptions.java +++ b/jbake-core/src/main/java/org/jbake/launcher/LaunchOptions.java @@ -1,41 +1,50 @@ package org.jbake.launcher; -import org.kohsuke.args4j.Argument; -import org.kohsuke.args4j.Option; +import picocli.CommandLine.ArgGroup; +import picocli.CommandLine.Command; +import picocli.CommandLine.Option; +import picocli.CommandLine.Parameters; import java.io.File; +@Command( + description = "JBake is a Java based, open source, static site/blog generator for developers & designers", + name = "jbake", + usageHelpAutoWidth = true +) public class LaunchOptions { - @Argument(index = 0, usage = "source folder of site content (with templates and assets), if not supplied will default to current directory", metaVar = "") + @Parameters(index = "0", description = "source folder of site content (with templates and assets), if not supplied will default to current directory", arity = "0..1") private String source; - @Argument(index = 1, usage = "destination folder for output, if not supplied will default to a folder called \"output\" in the current directory", metaVar = "") + @Parameters(index = "1", description = "destination folder for output, if not supplied will default to a folder called \"output\" in the current directory", arity = "0..1") private String destination; - @Option(name = "-b", aliases = {"--bake"}, usage = "performs a bake") + @Option(names = {"-b", "--bake"}, description = "performs a bake") private boolean bake; - @Option(name = "-i", aliases = {"--init"}, usage = "initialises required folder structure with default templates (defaults to current directory if is not supplied)") - private boolean init; + @ArgGroup(exclusive = false, heading = "%n%nJBake initialization%n%n") + private InitOptions initGroup; - @Option(name = "-t", aliases = {"--template"}, usage = "use specified template engine for default templates (uses Freemarker if is not supplied) ", depends = ("-i")) - private String template; + static class InitOptions { - @Option(name = "-s", aliases = {"--server"}, usage = "runs HTTP server to serve out baked site, if no is supplied will default to a folder called \"output\" in the current directory") + @Option(names = {"-i", "--init"}, paramLabel = "", description = "initialises required folder structure with default templates (defaults to current directory if is not supplied)", required = true) + private boolean init; + + @Option(names = {"-t", "--template"}, defaultValue = "freemarker", fallbackValue = "freemarker", description = "use specified template engine for default templates (uses Freemarker if