From e4b81e704db1906a46b718f68d8376289f6e310f Mon Sep 17 00:00:00 2001 From: Mathieu Baudier Date: Mon, 16 May 2022 07:21:20 +0200 Subject: [PATCH] Reintegrate CLI and FS sync. --- .../cms/acr/fs/BasicSyncFileVisitor.java | 162 ++++++++++++++++++ .../src/org/argeo/cms/acr/fs/FileSync.java | 101 +++++++++++ .../src/org/argeo/cms/acr/fs/FsCommands.java | 18 ++ .../src/org/argeo/cms/acr/fs/FsSyncUtils.java | 62 +++++++ .../src/org/argeo/cms/acr/fs/PathSync.java | 62 +++++++ .../org/argeo/cms/acr/fs/SyncFileVisitor.java | 30 ++++ .../src/org/argeo/cms/acr/fs/SyncResult.java | 101 +++++++++++ .../argeo/cms/cli/CommandArgsException.java | 32 ++++ .../cms/cli/CommandRuntimeException.java | 35 ++++ .../src/org/argeo/cms/cli/CommandsCli.java | 131 ++++++++++++++ .../org/argeo/cms/cli/DescribedCommand.java | 55 ++++++ .../src/org/argeo/cms/cli/HelpCommand.java | 143 ++++++++++++++++ .../src/org/argeo/cms/cli/package-info.java | 2 + .../runtime/DeployedContentRepository.java | 10 +- 14 files changed, 939 insertions(+), 5 deletions(-) create mode 100644 org.argeo.cms/src/org/argeo/cms/acr/fs/BasicSyncFileVisitor.java create mode 100644 org.argeo.cms/src/org/argeo/cms/acr/fs/FileSync.java create mode 100644 org.argeo.cms/src/org/argeo/cms/acr/fs/FsCommands.java create mode 100644 org.argeo.cms/src/org/argeo/cms/acr/fs/FsSyncUtils.java create mode 100644 org.argeo.cms/src/org/argeo/cms/acr/fs/PathSync.java create mode 100644 org.argeo.cms/src/org/argeo/cms/acr/fs/SyncFileVisitor.java create mode 100644 org.argeo.cms/src/org/argeo/cms/acr/fs/SyncResult.java create mode 100644 org.argeo.cms/src/org/argeo/cms/cli/CommandArgsException.java create mode 100644 org.argeo.cms/src/org/argeo/cms/cli/CommandRuntimeException.java create mode 100644 org.argeo.cms/src/org/argeo/cms/cli/CommandsCli.java create mode 100644 org.argeo.cms/src/org/argeo/cms/cli/DescribedCommand.java create mode 100644 org.argeo.cms/src/org/argeo/cms/cli/HelpCommand.java create mode 100644 org.argeo.cms/src/org/argeo/cms/cli/package-info.java diff --git a/org.argeo.cms/src/org/argeo/cms/acr/fs/BasicSyncFileVisitor.java b/org.argeo.cms/src/org/argeo/cms/acr/fs/BasicSyncFileVisitor.java new file mode 100644 index 000000000..7041c75d8 --- /dev/null +++ b/org.argeo.cms/src/org/argeo/cms/acr/fs/BasicSyncFileVisitor.java @@ -0,0 +1,162 @@ +package org.argeo.cms.acr.fs; + +import java.io.IOException; +import java.nio.file.FileVisitResult; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.SimpleFileVisitor; +import java.nio.file.StandardCopyOption; +import java.nio.file.attribute.BasicFileAttributes; +import java.nio.file.attribute.FileTime; + +/** Synchronises two directory structures. */ +public class BasicSyncFileVisitor extends SimpleFileVisitor { + // TODO make it configurable + private boolean trace = false; + + private final Path sourceBasePath; + private final Path targetBasePath; + private final boolean delete; + private final boolean recursive; + + private SyncResult syncResult = new SyncResult<>(); + + public BasicSyncFileVisitor(Path sourceBasePath, Path targetBasePath, boolean delete, boolean recursive) { + this.sourceBasePath = sourceBasePath; + this.targetBasePath = targetBasePath; + this.delete = delete; + this.recursive = recursive; + } + + @Override + public FileVisitResult preVisitDirectory(Path sourceDir, BasicFileAttributes attrs) throws IOException { + if (!recursive && !sourceDir.equals(sourceBasePath)) + return FileVisitResult.SKIP_SUBTREE; + Path targetDir = toTargetPath(sourceDir); + Files.createDirectories(targetDir); + return FileVisitResult.CONTINUE; + } + + @Override + public FileVisitResult postVisitDirectory(Path sourceDir, IOException exc) throws IOException { + if (delete) { + Path targetDir = toTargetPath(sourceDir); + for (Path targetPath : Files.newDirectoryStream(targetDir)) { + Path sourcePath = sourceDir.resolve(targetPath.getFileName()); + if (!Files.exists(sourcePath)) { + try { + FsSyncUtils.delete(targetPath); + deleted(targetPath); + } catch (Exception e) { + deleteFailed(targetPath, exc); + } + } + } + } + return FileVisitResult.CONTINUE; + } + + @Override + public FileVisitResult visitFile(Path sourceFile, BasicFileAttributes attrs) throws IOException { + Path targetFile = toTargetPath(sourceFile); + try { + if (!Files.exists(targetFile)) { + Files.copy(sourceFile, targetFile); + added(sourceFile, targetFile); + } else { + if (shouldOverwrite(sourceFile, targetFile)) { + Files.copy(sourceFile, targetFile, StandardCopyOption.REPLACE_EXISTING); + } + } + } catch (Exception e) { + copyFailed(sourceFile, targetFile, e); + } + return FileVisitResult.CONTINUE; + } + + protected boolean shouldOverwrite(Path sourceFile, Path targetFile) throws IOException { + long sourceSize = Files.size(sourceFile); + long targetSize = Files.size(targetFile); + if (sourceSize != targetSize) { + return true; + } + FileTime sourceLastModif = Files.getLastModifiedTime(sourceFile); + FileTime targetLastModif = Files.getLastModifiedTime(targetFile); + if (sourceLastModif.compareTo(targetLastModif) > 0) + return true; + return shouldOverwriteLaterSameSize(sourceFile, targetFile); + } + + protected boolean shouldOverwriteLaterSameSize(Path sourceFile, Path targetFile) { + return false; + } + +// @Override +// public FileVisitResult visitFileFailed(Path sourceFile, IOException exc) throws IOException { +// error("Cannot sync " + sourceFile, exc); +// return FileVisitResult.CONTINUE; +// } + + private Path toTargetPath(Path sourcePath) { + Path relativePath = sourceBasePath.relativize(sourcePath); + Path targetPath = targetBasePath.resolve(relativePath.toString()); + return targetPath; + } + + public Path getSourceBasePath() { + return sourceBasePath; + } + + public Path getTargetBasePath() { + return targetBasePath; + } + + protected void added(Path sourcePath, Path targetPath) { + syncResult.getAdded().add(targetPath); + if (isTraceEnabled()) + trace("Added " + sourcePath + " as " + targetPath); + } + + protected void modified(Path sourcePath, Path targetPath) { + syncResult.getModified().add(targetPath); + if (isTraceEnabled()) + trace("Overwritten from " + sourcePath + " to " + targetPath); + } + + protected void copyFailed(Path sourcePath, Path targetPath, Exception e) { + syncResult.addError(sourcePath, targetPath, e); + if (isTraceEnabled()) + error("Cannot copy " + sourcePath + " to " + targetPath, e); + } + + protected void deleted(Path targetPath) { + syncResult.getDeleted().add(targetPath); + if (isTraceEnabled()) + trace("Deleted " + targetPath); + } + + protected void deleteFailed(Path targetPath, Exception e) { + syncResult.addError(null, targetPath, e); + if (isTraceEnabled()) + error("Cannot delete " + targetPath, e); + } + + /** Log error. */ + protected void error(Object obj, Throwable e) { + System.err.println(obj); + e.printStackTrace(); + } + + protected boolean isTraceEnabled() { + return trace; + } + + protected void trace(Object obj) { + System.out.println(obj); + } + + public SyncResult getSyncResult() { + return syncResult; + } + +} diff --git a/org.argeo.cms/src/org/argeo/cms/acr/fs/FileSync.java b/org.argeo.cms/src/org/argeo/cms/acr/fs/FileSync.java new file mode 100644 index 000000000..230a831a9 --- /dev/null +++ b/org.argeo.cms/src/org/argeo/cms/acr/fs/FileSync.java @@ -0,0 +1,101 @@ +package org.argeo.cms.acr.fs; + +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; + +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.Option; +import org.apache.commons.cli.Options; +import org.argeo.cms.cli.CommandArgsException; +import org.argeo.cms.cli.DescribedCommand; + +public class FileSync implements DescribedCommand> { + final static Option deleteOption = Option.builder().longOpt("delete").desc("delete from target").build(); + final static Option recursiveOption = Option.builder("r").longOpt("recursive").desc("recurse into directories") + .build(); + final static Option progressOption = Option.builder().longOpt("progress").hasArg(false).desc("show progress") + .build(); + + @Override + public SyncResult apply(List t) { + try { + CommandLine line = toCommandLine(t); + List remaining = line.getArgList(); + if (remaining.size() == 0) { + throw new CommandArgsException("There must be at least one argument"); + } + URI sourceUri = new URI(remaining.get(0)); + URI targetUri; + if (remaining.size() == 1) { + targetUri = Paths.get(System.getProperty("user.dir")).toUri(); + } else { + targetUri = new URI(remaining.get(1)); + } + boolean delete = line.hasOption(deleteOption.getLongOpt()); + boolean recursive = line.hasOption(recursiveOption.getLongOpt()); + PathSync pathSync = new PathSync(sourceUri, targetUri, delete, recursive); + return pathSync.call(); + } catch (URISyntaxException e) { + throw new CommandArgsException(e); + } + } + + @Override + public Options getOptions() { + Options options = new Options(); + options.addOption(recursiveOption); + options.addOption(deleteOption); + options.addOption(progressOption); + return options; + } + + @Override + public String getUsage() { + return "[source URI] [target URI]"; + } + + public static void main(String[] args) { + DescribedCommand.mainImpl(new FileSync(), args); +// Options options = new Options(); +// options.addOption("r", "recursive", false, "recurse into directories"); +// options.addOption(Option.builder().longOpt("progress").hasArg(false).desc("show progress").build()); +// +// CommandLineParser parser = new DefaultParser(); +// try { +// CommandLine line = parser.parse(options, args); +// List remaining = line.getArgList(); +// if (remaining.size() == 0) { +// System.err.println("There must be at least one argument"); +// printHelp(options); +// System.exit(1); +// } +// URI sourceUri = new URI(remaining.get(0)); +// URI targetUri; +// if (remaining.size() == 1) { +// targetUri = Paths.get(System.getProperty("user.dir")).toUri(); +// } else { +// targetUri = new URI(remaining.get(1)); +// } +// PathSync pathSync = new PathSync(sourceUri, targetUri); +// pathSync.run(); +// } catch (Exception exp) { +// exp.printStackTrace(); +// printHelp(options); +// System.exit(1); +// } + } + +// public static void printHelp(Options options) { +// HelpFormatter formatter = new HelpFormatter(); +// formatter.printHelp("sync SRC [DEST]", options, true); +// } + + @Override + public String getDescription() { + return "Synchronises files"; + } + +} diff --git a/org.argeo.cms/src/org/argeo/cms/acr/fs/FsCommands.java b/org.argeo.cms/src/org/argeo/cms/acr/fs/FsCommands.java new file mode 100644 index 000000000..2aa240633 --- /dev/null +++ b/org.argeo.cms/src/org/argeo/cms/acr/fs/FsCommands.java @@ -0,0 +1,18 @@ +package org.argeo.cms.acr.fs; + +import org.argeo.cms.cli.CommandsCli; + +/** File utilities. */ +public class FsCommands extends CommandsCli { + + public FsCommands(String commandName) { + super(commandName); + addCommand("sync", new FileSync()); + } + + @Override + public String getDescription() { + return "Utilities around files and file systems"; + } + +} diff --git a/org.argeo.cms/src/org/argeo/cms/acr/fs/FsSyncUtils.java b/org.argeo.cms/src/org/argeo/cms/acr/fs/FsSyncUtils.java new file mode 100644 index 000000000..c45e66c3a --- /dev/null +++ b/org.argeo.cms/src/org/argeo/cms/acr/fs/FsSyncUtils.java @@ -0,0 +1,62 @@ +package org.argeo.cms.acr.fs; + +import java.io.IOException; +import java.nio.file.FileVisitResult; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.SimpleFileVisitor; +import java.nio.file.attribute.BasicFileAttributes; + +public class FsSyncUtils { + /** Sync a source path with a target path. */ + public static void sync(Path sourceBasePath, Path targetBasePath) { + sync(sourceBasePath, targetBasePath, false); + } + + /** Sync a source path with a target path. */ + public static void sync(Path sourceBasePath, Path targetBasePath, boolean delete) { + sync(new BasicSyncFileVisitor(sourceBasePath, targetBasePath, delete, true)); + } + + public static void sync(BasicSyncFileVisitor syncFileVisitor) { + try { + Files.walkFileTree(syncFileVisitor.getSourceBasePath(), syncFileVisitor); + } catch (Exception e) { + throw new RuntimeException("Cannot sync " + syncFileVisitor.getSourceBasePath() + " with " + + syncFileVisitor.getTargetBasePath(), e); + } + } + + /** + * Deletes this path, recursively if needed. Does nothing if the path does not + * exist. + */ + public static void delete(Path path) { + try { + if (!Files.exists(path)) + return; + Files.walkFileTree(path, new SimpleFileVisitor() { + @Override + public FileVisitResult postVisitDirectory(Path directory, IOException e) throws IOException { + if (e != null) + throw e; + Files.delete(directory); + return FileVisitResult.CONTINUE; + } + + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { + Files.delete(file); + return FileVisitResult.CONTINUE; + } + }); + } catch (IOException e) { + throw new RuntimeException("Cannot delete " + path, e); + } + } + + /** Singleton. */ + private FsSyncUtils() { + + } +} diff --git a/org.argeo.cms/src/org/argeo/cms/acr/fs/PathSync.java b/org.argeo.cms/src/org/argeo/cms/acr/fs/PathSync.java new file mode 100644 index 000000000..d4f688187 --- /dev/null +++ b/org.argeo.cms/src/org/argeo/cms/acr/fs/PathSync.java @@ -0,0 +1,62 @@ +package org.argeo.cms.acr.fs; + +import java.net.URI; +import java.nio.file.FileSystems; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.spi.FileSystemProvider; +import java.util.concurrent.Callable; + +import org.argeo.cms.acr.fs.SyncFileVisitor; +import org.argeo.cms.acr.fs.SyncResult; + +/** Synchronises two paths. */ +public class PathSync implements Callable> { + private final URI sourceUri, targetUri; + private final boolean delete; + private final boolean recursive; + + public PathSync(URI sourceUri, URI targetUri) { + this(sourceUri, targetUri, false, false); + } + + public PathSync(URI sourceUri, URI targetUri, boolean delete, boolean recursive) { + this.sourceUri = sourceUri; + this.targetUri = targetUri; + this.delete = delete; + this.recursive = recursive; + } + + @Override + public SyncResult call() { + try { + Path sourceBasePath = createPath(sourceUri); + Path targetBasePath = createPath(targetUri); + SyncFileVisitor syncFileVisitor = new SyncFileVisitor(sourceBasePath, targetBasePath, delete, recursive); + Files.walkFileTree(sourceBasePath, syncFileVisitor); + return syncFileVisitor.getSyncResult(); + } catch (Exception e) { + throw new IllegalStateException("Cannot sync " + sourceUri + " to " + targetUri, e); + } + } + + private Path createPath(URI uri) { + Path path; + if (uri.getScheme() == null) { + path = Paths.get(uri.getPath()); + } else if (uri.getScheme().equals("file")) { + FileSystemProvider fsProvider = FileSystems.getDefault().provider(); + path = fsProvider.getPath(uri); + } else if (uri.getScheme().equals("davex")) { + throw new UnsupportedOperationException(); +// FileSystemProvider fsProvider = new DavexFsProvider(); +// path = fsProvider.getPath(uri); +// } else if (uri.getScheme().equals("sftp")) { +// Sftp sftp = new Sftp(uri); +// path = sftp.getBasePath(); + } else + throw new IllegalArgumentException("URI scheme not supported for " + uri); + return path; + } +} diff --git a/org.argeo.cms/src/org/argeo/cms/acr/fs/SyncFileVisitor.java b/org.argeo.cms/src/org/argeo/cms/acr/fs/SyncFileVisitor.java new file mode 100644 index 000000000..1370702cb --- /dev/null +++ b/org.argeo.cms/src/org/argeo/cms/acr/fs/SyncFileVisitor.java @@ -0,0 +1,30 @@ +package org.argeo.cms.acr.fs; + +import java.lang.System.Logger; +import java.lang.System.Logger.Level; +import java.nio.file.Path; +import java.util.Objects; + +/** Synchronises two directory structures. */ +public class SyncFileVisitor extends BasicSyncFileVisitor { + private final static Logger logger = System.getLogger(SyncFileVisitor.class.getName()); + + public SyncFileVisitor(Path sourceBasePath, Path targetBasePath, boolean delete, boolean recursive) { + super(sourceBasePath, targetBasePath, delete, recursive); + } + + @Override + protected void error(Object obj, Throwable e) { + logger.log(Level.ERROR, Objects.toString(obj), e); + } + + @Override + protected boolean isTraceEnabled() { + return logger.isLoggable(Level.TRACE); + } + + @Override + protected void trace(Object obj) { + logger.log(Level.TRACE, Objects.toString(obj)); + } +} diff --git a/org.argeo.cms/src/org/argeo/cms/acr/fs/SyncResult.java b/org.argeo.cms/src/org/argeo/cms/acr/fs/SyncResult.java new file mode 100644 index 000000000..709b485ff --- /dev/null +++ b/org.argeo.cms/src/org/argeo/cms/acr/fs/SyncResult.java @@ -0,0 +1,101 @@ +package org.argeo.cms.acr.fs; + +import java.time.Instant; +import java.util.Set; +import java.util.TreeSet; + +/** Describes what happendend during a sync operation. */ +public class SyncResult { + private final Set added = new TreeSet<>(); + private final Set modified = new TreeSet<>(); + private final Set deleted = new TreeSet<>(); + private final Set errors = new TreeSet<>(); + + public Set getAdded() { + return added; + } + + public Set getModified() { + return modified; + } + + public Set getDeleted() { + return deleted; + } + + public Set getErrors() { + return errors; + } + + public void addError(T sourcePath, T targetPath, Exception e) { + Error error = new Error(sourcePath, targetPath, e); + errors.add(error); + } + + public boolean noModification() { + return modified.isEmpty() && deleted.isEmpty() && added.isEmpty(); + } + + @Override + public String toString() { + if (noModification()) + return "No modification."; + StringBuffer sb = new StringBuffer(); + for (T p : modified) + sb.append("MOD ").append(p).append('\n'); + for (T p : deleted) + sb.append("DEL ").append(p).append('\n'); + for (T p : added) + sb.append("ADD ").append(p).append('\n'); + for (Error error : errors) + sb.append(error).append('\n'); + return sb.toString(); + } + + public class Error implements Comparable { + private final T sourcePath;// if null this is a failed delete + private final T targetPath; + private final Exception exception; + private final Instant timestamp = Instant.now(); + + public Error(T sourcePath, T targetPath, Exception e) { + super(); + this.sourcePath = sourcePath; + this.targetPath = targetPath; + this.exception = e; + } + + public T getSourcePath() { + return sourcePath; + } + + public T getTargetPath() { + return targetPath; + } + + public Exception getException() { + return exception; + } + + public Instant getTimestamp() { + return timestamp; + } + + @Override + public int compareTo(Error o) { + return timestamp.compareTo(o.timestamp); + } + + @Override + public int hashCode() { + return timestamp.hashCode(); + } + + @Override + public String toString() { + return "ERR " + timestamp + (sourcePath == null ? "Deletion failed" : "Copy failed " + sourcePath) + " " + + targetPath + " " + exception.getMessage(); + } + + } +} diff --git a/org.argeo.cms/src/org/argeo/cms/cli/CommandArgsException.java b/org.argeo.cms/src/org/argeo/cms/cli/CommandArgsException.java new file mode 100644 index 000000000..1f6d56bb1 --- /dev/null +++ b/org.argeo.cms/src/org/argeo/cms/cli/CommandArgsException.java @@ -0,0 +1,32 @@ +package org.argeo.cms.cli; + +public class CommandArgsException extends IllegalArgumentException { + private static final long serialVersionUID = -7271050747105253935L; + private String commandName; + private volatile CommandsCli commandsCli; + + public CommandArgsException(Exception cause) { + super(cause.getMessage(), cause); + } + + public CommandArgsException(String message) { + super(message); + } + + public String getCommandName() { + return commandName; + } + + public void setCommandName(String commandName) { + this.commandName = commandName; + } + + public CommandsCli getCommandsCli() { + return commandsCli; + } + + public void setCommandsCli(CommandsCli commandsCli) { + this.commandsCli = commandsCli; + } + +} diff --git a/org.argeo.cms/src/org/argeo/cms/cli/CommandRuntimeException.java b/org.argeo.cms/src/org/argeo/cms/cli/CommandRuntimeException.java new file mode 100644 index 000000000..ef27c1fc8 --- /dev/null +++ b/org.argeo.cms/src/org/argeo/cms/cli/CommandRuntimeException.java @@ -0,0 +1,35 @@ +package org.argeo.cms.cli; + +import java.util.List; + +/** {@link RuntimeException} referring during a command run. */ +public class CommandRuntimeException extends RuntimeException { + private static final long serialVersionUID = 5595999301269377128L; + + private final DescribedCommand command; + private final List arguments; + + public CommandRuntimeException(Throwable e, DescribedCommand command, List arguments) { + this(null, e, command, arguments); + } + + public CommandRuntimeException(String message, DescribedCommand command, List arguments) { + this(message, null, command, arguments); + } + + public CommandRuntimeException(String message, Throwable e, DescribedCommand command, List arguments) { + super(message == null ? "(" + command.getClass().getName() + " " + arguments.toString() + ")" + : message + " (" + command.getClass().getName() + " " + arguments.toString() + ")", e); + this.command = command; + this.arguments = arguments; + } + + public DescribedCommand getCommand() { + return command; + } + + public List getArguments() { + return arguments; + } + +} diff --git a/org.argeo.cms/src/org/argeo/cms/cli/CommandsCli.java b/org.argeo.cms/src/org/argeo/cms/cli/CommandsCli.java new file mode 100644 index 000000000..d45ea33a0 --- /dev/null +++ b/org.argeo.cms/src/org/argeo/cms/cli/CommandsCli.java @@ -0,0 +1,131 @@ +package org.argeo.cms.cli; + +import java.io.StringWriter; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; +import java.util.function.Function; + +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.CommandLineParser; +import org.apache.commons.cli.DefaultParser; +import org.apache.commons.cli.Options; +import org.apache.commons.cli.ParseException; + +/** Base class for a CLI managing sub commands. */ +public abstract class CommandsCli implements DescribedCommand { + public final static String HELP = "help"; + + private final String commandName; + private Map, ?>> commands = new TreeMap<>(); + + protected final Options options = new Options(); + + public CommandsCli(String commandName) { + this.commandName = commandName; + } + + @Override + public Object apply(List args) { + String cmd = null; + List newArgs = new ArrayList<>(); + try { + CommandLineParser clParser = new DefaultParser(); + CommandLine commonCl = clParser.parse(getOptions(), args.toArray(new String[args.size()]), true); + List leftOvers = commonCl.getArgList(); + for (String arg : leftOvers) { + if (!arg.startsWith("-") && cmd == null) { + cmd = arg; + } else { + newArgs.add(arg); + } + } + } catch (ParseException e) { + CommandArgsException cae = new CommandArgsException(e); + throw cae; + } + + Function, ?> function = cmd != null ? getCommand(cmd) : getDefaultCommand(); + if (function == null) + throw new IllegalArgumentException("Uknown command " + cmd); + try { + return function.apply(newArgs).toString(); + } catch (CommandArgsException e) { + if (e.getCommandName() == null) { + e.setCommandName(cmd); + e.setCommandsCli(this); + } + throw e; + } catch (IllegalArgumentException e) { + CommandArgsException cae = new CommandArgsException(e); + cae.setCommandName(cmd); + throw cae; + } + } + + @Override + public Options getOptions() { + return options; + } + + protected void addCommand(String cmd, Function, ?> function) { + commands.put(cmd, function); + + } + + @Override + public String getUsage() { + return "[command]"; + } + + protected void addCommandsCli(CommandsCli commandsCli) { + addCommand(commandsCli.getCommandName(), commandsCli); + commandsCli.addCommand(HELP, new HelpCommand(this, commandsCli)); + } + + public String getCommandName() { + return commandName; + } + + public Set getSubCommands() { + return commands.keySet(); + } + + public Function, ?> getCommand(String command) { + return commands.get(command); + } + + public HelpCommand getHelpCommand() { + return (HelpCommand) getCommand(HELP); + } + + public Function, String> getDefaultCommand() { + return getHelpCommand(); + } + + /** In order to implement quickly a main method. */ + public static void mainImpl(CommandsCli cli, String[] args) { + try { + cli.addCommand(CommandsCli.HELP, new HelpCommand(null, cli)); + Object output = cli.apply(Arrays.asList(args)); + System.out.println(output); + System.exit(0); + } catch (CommandArgsException e) { + System.err.println("Wrong arguments " + Arrays.toString(args) + ": " + e.getMessage()); + if (e.getCommandName() != null) { + StringWriter out = new StringWriter(); + HelpCommand.printHelp(e.getCommandsCli(), e.getCommandName(), out); + System.err.println(out.toString()); + } else { + e.printStackTrace(); + } + System.exit(1); + } catch (Exception e) { + e.printStackTrace(); + System.exit(1); + } + } +} diff --git a/org.argeo.cms/src/org/argeo/cms/cli/DescribedCommand.java b/org.argeo.cms/src/org/argeo/cms/cli/DescribedCommand.java new file mode 100644 index 000000000..cdfe1300d --- /dev/null +++ b/org.argeo.cms/src/org/argeo/cms/cli/DescribedCommand.java @@ -0,0 +1,55 @@ +package org.argeo.cms.cli; + +import java.io.StringWriter; +import java.util.Arrays; +import java.util.List; +import java.util.function.Function; + +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.DefaultParser; +import org.apache.commons.cli.Options; +import org.apache.commons.cli.ParseException; + +/** A command that can be described. */ +public interface DescribedCommand extends Function, T> { + default Options getOptions() { + return new Options(); + } + + String getDescription(); + + default String getUsage() { + return null; + } + + default String getExamples() { + return null; + } + + default CommandLine toCommandLine(List args) { + try { + DefaultParser parser = new DefaultParser(); + return parser.parse(getOptions(), args.toArray(new String[args.size()])); + } catch (ParseException e) { + throw new CommandArgsException(e); + } + } + + /** In order to implement quickly a main method. */ + public static void mainImpl(DescribedCommand command, String[] args) { + try { + Object output = command.apply(Arrays.asList(args)); + System.out.println(output); + System.exit(0); + } catch (IllegalArgumentException e) { + StringWriter out = new StringWriter(); + HelpCommand.printHelp(command, out); + System.err.println(out.toString()); + System.exit(1); + } catch (Exception e) { + e.printStackTrace(); + System.exit(1); + } + } + +} diff --git a/org.argeo.cms/src/org/argeo/cms/cli/HelpCommand.java b/org.argeo.cms/src/org/argeo/cms/cli/HelpCommand.java new file mode 100644 index 000000000..ab899f437 --- /dev/null +++ b/org.argeo.cms/src/org/argeo/cms/cli/HelpCommand.java @@ -0,0 +1,143 @@ +package org.argeo.cms.cli; + +import java.io.PrintWriter; +import java.io.StringWriter; +import java.util.List; +import java.util.function.Function; + +import org.apache.commons.cli.HelpFormatter; +import org.apache.commons.cli.Options; + +/** A special command that can describe {@link DescribedCommand}. */ +public class HelpCommand implements DescribedCommand { + private CommandsCli commandsCli; + private CommandsCli parentCommandsCli; + + // Help formatting + private static int helpWidth = 80; + private static int helpLeftPad = 4; + private static int helpDescPad = 20; + + public HelpCommand(CommandsCli parentCommandsCli, CommandsCli commandsCli) { + super(); + this.parentCommandsCli = parentCommandsCli; + this.commandsCli = commandsCli; + } + + @Override + public String apply(List args) { + StringWriter out = new StringWriter(); + + if (args.size() == 0) {// overview + printHelp(commandsCli, out); + } else { + String cmd = args.get(0); + Function, ?> function = commandsCli.getCommand(cmd); + if (function == null) + return "Command " + cmd + " not found."; + Options options; + String examples; + DescribedCommand command = null; + if (function instanceof DescribedCommand) { + command = (DescribedCommand) function; + options = command.getOptions(); + examples = command.getExamples(); + } else { + options = new Options(); + examples = null; + } + String description = getShortDescription(function); + String commandCall = getCommandUsage(cmd, command); + HelpFormatter formatter = new HelpFormatter(); + formatter.printHelp(new PrintWriter(out), helpWidth, commandCall, description, options, helpLeftPad, + helpDescPad, examples, false); + } + return out.toString(); + } + + private static String getShortDescription(Function, ?> function) { + if (function instanceof DescribedCommand) { + return ((DescribedCommand) function).getDescription(); + } else { + return function.toString(); + } + } + + public String getCommandUsage(String cmd, DescribedCommand command) { + String commandCall = getCommandCall(commandsCli) + " " + cmd; + assert command != null; + if (command != null && command.getUsage() != null) { + commandCall = commandCall + " " + command.getUsage(); + } + return commandCall; + } + + @Override + public String getDescription() { + return "Shows this help or describes a command"; + } + + @Override + public String getUsage() { + return "[command]"; + } + + public CommandsCli getParentCommandsCli() { + return parentCommandsCli; + } + + protected String getCommandCall(CommandsCli commandsCli) { + HelpCommand hc = commandsCli.getHelpCommand(); + if (hc.getParentCommandsCli() != null) { + return getCommandCall(hc.getParentCommandsCli()) + " " + commandsCli.getCommandName(); + } else { + return commandsCli.getCommandName(); + } + } + + public static void printHelp(DescribedCommand command, StringWriter out) { + String usage = "java " + command.getClass().getName() + + (command.getUsage() != null ? " " + command.getUsage() : ""); + HelpFormatter formatter = new HelpFormatter(); + formatter.printHelp(new PrintWriter(out), helpWidth, usage, command.getDescription(), command.getOptions(), + helpLeftPad, helpDescPad, command.getExamples(), false); + + } + + public static void printHelp(CommandsCli commandsCli, String commandName, StringWriter out) { + DescribedCommand command = (DescribedCommand) commandsCli.getCommand(commandName); + String usage = commandsCli.getHelpCommand().getCommandUsage(commandName, command); + HelpFormatter formatter = new HelpFormatter(); + formatter.printHelp(new PrintWriter(out), helpWidth, usage, command.getDescription(), command.getOptions(), + helpLeftPad, helpDescPad, command.getExamples(), false); + + } + + public static void printHelp(CommandsCli commandsCli, StringWriter out) { + out.append(commandsCli.getDescription()).append('\n'); + String leftPad = spaces(helpLeftPad); + for (String cmd : commandsCli.getSubCommands()) { + Function, ?> function = commandsCli.getCommand(cmd); + assert function != null; + out.append(leftPad); + out.append(cmd); + // TODO deal with long commands + out.append(spaces(helpDescPad - cmd.length())); + out.append(getShortDescription(function)); + out.append('\n'); + } + } + + private static String spaces(int count) { + // Java 11 + // return " ".repeat(count); + if (count <= 0) + return ""; + else { + StringBuilder sb = new StringBuilder(count); + for (int i = 0; i < count; i++) + sb.append(' '); + return sb.toString(); + } + } +} diff --git a/org.argeo.cms/src/org/argeo/cms/cli/package-info.java b/org.argeo.cms/src/org/argeo/cms/cli/package-info.java new file mode 100644 index 000000000..59feed1d1 --- /dev/null +++ b/org.argeo.cms/src/org/argeo/cms/cli/package-info.java @@ -0,0 +1,2 @@ +/** Command line API. */ +package org.argeo.cms.cli; \ No newline at end of file diff --git a/org.argeo.cms/src/org/argeo/cms/internal/runtime/DeployedContentRepository.java b/org.argeo.cms/src/org/argeo/cms/internal/runtime/DeployedContentRepository.java index 6392fdaf3..e98798d7c 100644 --- a/org.argeo.cms/src/org/argeo/cms/internal/runtime/DeployedContentRepository.java +++ b/org.argeo.cms/src/org/argeo/cms/internal/runtime/DeployedContentRepository.java @@ -11,7 +11,7 @@ import org.argeo.cms.acr.fs.FsContentProvider; import org.argeo.util.LangUtils; public class DeployedContentRepository extends CmsContentRepository { - private final static String ROOT_XML = "root.xml"; + private final static String ROOT_XML = "cr:root.xml"; private final static String ACR_MOUNT_PATH = "acr.mount.path"; private CmsState cmsState; @@ -19,12 +19,12 @@ public class DeployedContentRepository extends CmsContentRepository { @Override public void start() { super.start(); - Path rootXml = KernelUtils.getOsgiInstancePath(KernelConstants.DIR_NODE).resolve(ROOT_XML); + Path rootXml = KernelUtils.getOsgiInstancePath(ROOT_XML); initRootContentProvider(rootXml); - Path srvPath = KernelUtils.getOsgiInstancePath(CmsConstants.SRV_WORKSPACE); - FsContentProvider srvContentProvider = new FsContentProvider(srvPath, false); - addProvider("/" + CmsConstants.SRV_WORKSPACE, srvContentProvider); +// Path srvPath = KernelUtils.getOsgiInstancePath(CmsConstants.SRV_WORKSPACE); +// FsContentProvider srvContentProvider = new FsContentProvider(srvPath, false); +// addProvider("/" + CmsConstants.SRV_WORKSPACE, srvContentProvider); } @Override -- 2.30.2