// SPDX-License-Identifier: BSD-3-Clause package org.islandoftex.texplate.model; import io.vavr.control.Either; import org.islandoftex.texplate.util.MergingUtils; import org.islandoftex.texplate.util.MessageUtils; import org.islandoftex.texplate.util.PathUtils; import org.islandoftex.texplate.util.ValidatorUtils; import picocli.CommandLine; import picocli.CommandLine.Option; import java.nio.file.Path; import java.util.HashMap; import java.util.Map; import java.util.concurrent.Callable; /** * The template processing class. * * @version 1.0 * @since 1.0 */ @CommandLine.Command( usageHelpWidth = 70, name = "texplate" ) public class TemplateProcessing implements Callable { // the file output, which will hold the // result of the merging of both template // and context data map from command line @Option( names = {"-o", "--output"}, description = "The output file in which the chosen " + "template will be effectively written. Make sure " + "the directory has the correct permissions for " + "writing the output file.", required = true, type = Path.class ) private Path output; // the template name @Option( names = {"-t", "--template"}, description = "The template and. The tool will " + "search both user and system locations and set the " + "template model accordingly, based on your specs." ) private String template; // the context data map that holds // a set of key/value pairs to be // merged with the template @Option( names = {"-m", "--map"}, description = "The contextual map that provides the " + "data to be merged in the template. This parameter " + "can be used multiple times. You can specify a map " + "entry with the key=value syntax (mind the entry " + "separator).", arity = "1..*" ) private Map map; @Option( names = {"-c", "--config"}, description = "The configuration file in which the tool " + "can read template data, for automation purposes. Make " + "sure to follow the correct specification when writing " + "a configuration file.", type = Path.class ) private Path configuration; /** * The application logic, enclosed as a call method. * * @return An integer value denoting the exit status. * @throws Exception An exception was raised in the application logic. */ @Override public Integer call() throws Exception { // the exit status, originally // set as a valid value int exit = 0; // configuration halt flag, indicating // whether the tool has to end earlier boolean halt = false; Map cmap = new HashMap<>(); // ensure the context data map // is at least instantiated ensureMap(); // there is a configuration file // found in the command line if (has(configuration)) { // print the configuration // check line for status MessageUtils.line("Checking configuration"); // try to read the configuration // file into a proper object Either configChecking = Configuration. of(configuration).toEither(); // the configuration file // seems to be valid, proceed if (configChecking.isRight()) { // print status line MessageUtils.status(true); // get the configuration file Configuration config = configChecking.get(); // check if the configuration // has a proper template if (has(config.getTemplate())) { // if so, build the template if, and // only if, there's no one already // set through command line template = ensure(template, config.getTemplate()); } // check if the configuration // has a proper string/string map if (has(config.getMap())) { // set the main configuration // map to be dealt later on cmap = config.getMap(); } // print header about tidying // up configuration variables MessageUtils.line("Adjusting variables from file"); MessageUtils.status(true); System.out.println(); } else { // an error occurred, print it // set exit code and halt MessageUtils.status(false); MessageUtils.error(configChecking.getLeft()); exit = -1; halt = true; } } else { // print header regarding // no config file found MessageUtils.line("Configuration file mode disabled"); MessageUtils.status(true); // print a header regarding // full command line mode MessageUtils.line("Entering full command line mode"); // there's no configuration file, so we // need to check whether there is not a // pattern set in the command line if (!has(template)) { // print status MessageUtils.status(false); // print message MessageUtils.error(new Exception("The template was not set " + "in the command line through the -t/--template " + "option. If not explicitly specified in a " + "configuration file, this option becomes mandatory, " + "so make sure to define it either in the command " + "line or in a proper configuration file.")); exit = -1; halt = true; } else { // print status MessageUtils.status(true); System.out.println(); } } // check whether we should // halt prematurely if (!halt) { // initial message, preparing our // hearts to the actual merging :) System.out.println("Please, wait..."); System.out.println(); // now we need to obtain the actual // template from a file stored either // in the user home or in the system MessageUtils.line("Obtaining reference"); // let us try to get the corresponding // file from the template pattern Either fileChecking = PathUtils. getTemplatePath(template).toEither(); // the actual template file was // found, so we can proceed to // the next phase if (fileChecking.isRight()) { // updates the current // status accordingly MessageUtils.status(true); // now it's time to compose the template // object from its corresponding file MessageUtils.line("Composing template"); // attempts to retrieve the template // attributes from the referenced file // to the actual template object Either templateComposition = Template.of(fileChecking.get()).toEither(); // the template composition was successful, // so we can move on to the next phase if (templateComposition.isRight()) { // updates the current // status accordingly MessageUtils.status(true); // once the template object is populated, // we need to verify if both template and // data map are not somehow conflicting MessageUtils.line("Validating data"); Either> dataValidation = ValidatorUtils.validate(templateComposition.get(), map).toEither(); // the data validation was consistent, // so now the merging can be applied if (dataValidation.isRight()) { // updates the current // status accordingly MessageUtils.status(true); // now it's the final phase, in which // both template and data are merged MessageUtils.line("Merging template and data"); // merge both template and context data // map Either merging = MergingUtils. merge(templateComposition.get(), dataValidation.get(), output, cmap).toEither(); // the merging was successful, // so now there's nothing else // to do, yay! if (merging.isRight()) { // updates the current // status accordingly MessageUtils.status(true); // print the final messge and // tell the user everything // went smooth! System.out.println(); System.out.println("Done! Enjoy your template!"); System.out.println("Written: " + getSize(merging.get())); } else { // updates the current // status accordingly MessageUtils.status(false); // the merging failed, so the // exception is displayed and // the exit status is updated MessageUtils.error(merging.getLeft()); exit = -1; } } else { // updates the current // status accordingly MessageUtils.status(false); // the data validation failed, so // the exception is displayed and // the exit status is updated MessageUtils.error(dataValidation.getLeft()); exit = -1; } } else { // updates the current // status accordingly MessageUtils.status(false); // the template composition failed, // so the exception is displayed and // the exit status is updated MessageUtils.error(templateComposition.getLeft()); exit = -1; } } else { // updates the current // status accordingly MessageUtils.status(false); // the file checking failed, so // the exception is displayed and // the exit status is updated MessageUtils.error(fileChecking.getLeft()); exit = -1; } } // the exit status is returned, // denoting whether the application // was able to merge both template // and data accordingly return exit; } /** * Ensures the data map is never pointed to a null reference. */ private void ensureMap() { // if the map is null, simply // create a new instance if (!has(map)) { map = new HashMap<>(); } } /** * Gets the file size in a human readable format. * * @param bytes The file size, in bytes. * @return The file size in a human readable format. */ private String getSize(long bytes) { if (bytes < 1024) { return bytes + " B"; } else { int exponent = (int) (Math.log(bytes) / Math.log(1024)); return String.format("%.1f %cB", bytes / Math.pow(1024, exponent), "KMGTPE".charAt(exponent - 1)); } } /** * Ensures the first parameter is not null, or sets it to the second one. * * @param The type. * @param first First parameter. * @param second Second parameter. * @return Either the first or the second one. */ private T ensure(T first, T second) { return !has(first) ? second : first; } /** * Checks whether the object exists. * * @param object The objects. * @return Boolean value indicating whether the object exists. */ private boolean has(Object object) { return object != null; } }