Reintegrate CLI and FS sync.
authorMathieu Baudier <mbaudier@argeo.org>
Mon, 16 May 2022 05:21:20 +0000 (07:21 +0200)
committerMathieu Baudier <mbaudier@argeo.org>
Mon, 16 May 2022 05:21:20 +0000 (07:21 +0200)
14 files changed:
org.argeo.cms/src/org/argeo/cms/acr/fs/BasicSyncFileVisitor.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/acr/fs/FileSync.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/acr/fs/FsCommands.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/acr/fs/FsSyncUtils.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/acr/fs/PathSync.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/acr/fs/SyncFileVisitor.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/acr/fs/SyncResult.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/cli/CommandArgsException.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/cli/CommandRuntimeException.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/cli/CommandsCli.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/cli/DescribedCommand.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/cli/HelpCommand.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/cli/package-info.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/internal/runtime/DeployedContentRepository.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 (file)
index 0000000..7041c75
--- /dev/null
@@ -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<Path> {
+       // 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<Path> 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<Path> 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 (file)
index 0000000..230a831
--- /dev/null
@@ -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<SyncResult<Path>> {
+       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<Path> apply(List<String> t) {
+               try {
+                       CommandLine line = toCommandLine(t);
+                       List<String> 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<String> 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 (file)
index 0000000..2aa2406
--- /dev/null
@@ -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 (file)
index 0000000..c45e66c
--- /dev/null
@@ -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<Path>() {
+                               @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 (file)
index 0000000..d4f6881
--- /dev/null
@@ -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<SyncResult<Path>> {
+       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<Path> 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 (file)
index 0000000..1370702
--- /dev/null
@@ -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 (file)
index 0000000..709b485
--- /dev/null
@@ -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<T> {
+       private final Set<T> added = new TreeSet<>();
+       private final Set<T> modified = new TreeSet<>();
+       private final Set<T> deleted = new TreeSet<>();
+       private final Set<Error> errors = new TreeSet<>();
+
+       public Set<T> getAdded() {
+               return added;
+       }
+
+       public Set<T> getModified() {
+               return modified;
+       }
+
+       public Set<T> getDeleted() {
+               return deleted;
+       }
+
+       public Set<Error> 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<Error> {
+               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 (file)
index 0000000..1f6d56b
--- /dev/null
@@ -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 (file)
index 0000000..ef27c1f
--- /dev/null
@@ -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<String> arguments;
+
+       public CommandRuntimeException(Throwable e, DescribedCommand<?> command, List<String> arguments) {
+               this(null, e, command, arguments);
+       }
+
+       public CommandRuntimeException(String message, DescribedCommand<?> command, List<String> arguments) {
+               this(message, null, command, arguments);
+       }
+
+       public CommandRuntimeException(String message, Throwable e, DescribedCommand<?> command, List<String> 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<String> 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 (file)
index 0000000..d45ea33
--- /dev/null
@@ -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<Object> {
+       public final static String HELP = "help";
+
+       private final String commandName;
+       private Map<String, Function<List<String>, ?>> commands = new TreeMap<>();
+
+       protected final Options options = new Options();
+
+       public CommandsCli(String commandName) {
+               this.commandName = commandName;
+       }
+
+       @Override
+       public Object apply(List<String> args) {
+               String cmd = null;
+               List<String> newArgs = new ArrayList<>();
+               try {
+                       CommandLineParser clParser = new DefaultParser();
+                       CommandLine commonCl = clParser.parse(getOptions(), args.toArray(new String[args.size()]), true);
+                       List<String> 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<List<String>, ?> 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<List<String>, ?> 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<String> getSubCommands() {
+               return commands.keySet();
+       }
+
+       public Function<List<String>, ?> getCommand(String command) {
+               return commands.get(command);
+       }
+
+       public HelpCommand getHelpCommand() {
+               return (HelpCommand) getCommand(HELP);
+       }
+
+       public Function<List<String>, 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 (file)
index 0000000..cdfe130
--- /dev/null
@@ -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<T> extends Function<List<String>, T> {
+       default Options getOptions() {
+               return new Options();
+       }
+
+       String getDescription();
+
+       default String getUsage() {
+               return null;
+       }
+
+       default String getExamples() {
+               return null;
+       }
+
+       default CommandLine toCommandLine(List<String> 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 (file)
index 0000000..ab899f4
--- /dev/null
@@ -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<String> {
+       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<String> args) {
+               StringWriter out = new StringWriter();
+
+               if (args.size() == 0) {// overview
+                       printHelp(commandsCli, out);
+               } else {
+                       String cmd = args.get(0);
+                       Function<List<String>, ?> 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<List<String>, ?> 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<List<String>, ?> 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 (file)
index 0000000..59feed1
--- /dev/null
@@ -0,0 +1,2 @@
+/** Command line API. */
+package org.argeo.cms.cli;
\ No newline at end of file
index 6392fdaf3534fd5eab61df9f8a2270fa860cb73b..e98798d7c98f338e8c95275d56c1a4ad9ca39521 100644 (file)
@@ -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