Move CLI utilities from Argeo Commons
authorMathieu Baudier <mbaudier@argeo.org>
Thu, 9 Dec 2021 11:11:59 +0000 (12:11 +0100)
committerMathieu Baudier <mbaudier@argeo.org>
Thu, 9 Dec 2021 11:11:59 +0000 (12:11 +0100)
42 files changed:
dep/org.argeo.slc.dep.minimal/pom.xml
legacy/argeo-commons/dep/org.argeo.dep.cms.platform/pom.xml
legacy/argeo-commons/org.argeo.cms.ui.workbench.rap/pom.xml
legacy/argeo-commons/org.argeo.cms.ui.workbench/pom.xml
legacy/argeo-commons/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/internal/jcr/commands/DumpNode.java
legacy/argeo-commons/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/jcr/WorkbenchJcrDClickListener.java
legacy/argeo-commons/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/legacy/rap/OpenFile.java [new file with mode: 0644]
legacy/argeo-commons/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/legacy/rap/OpenFileService.java [new file with mode: 0644]
legacy/argeo-commons/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/legacy/rap/SingleSourcingConstants.java [new file with mode: 0644]
legacy/argeo-commons/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/legacy/rap/SingleSourcingException.java [new file with mode: 0644]
legacy/org.argeo.slc.client.ui.dist/pom.xml
legacy/org.argeo.slc.client.ui/pom.xml
org.argeo.slc.api/bnd.bnd
org.argeo.slc.jcr/bnd.bnd
org.argeo.slc.jcr/build.properties
org.argeo.slc.jcr/src/org/argeo/cli/jcr/JcrCommands.java [new file with mode: 0644]
org.argeo.slc.jcr/src/org/argeo/cli/jcr/JcrSync.java [new file with mode: 0644]
org.argeo.slc.jcr/src/org/argeo/cli/jcr/package-info.java [new file with mode: 0644]
org.argeo.slc.jcr/src/org/argeo/cli/jcr/repository-localfs.xml [new file with mode: 0644]
org.argeo.slc.runtime/.classpath
org.argeo.slc.runtime/ext/test/org/argeo/fs/FsUtilsTest.java [new file with mode: 0644]
org.argeo.slc.runtime/src/org/argeo/cli/CommandArgsException.java [new file with mode: 0644]
org.argeo.slc.runtime/src/org/argeo/cli/CommandRuntimeException.java [new file with mode: 0644]
org.argeo.slc.runtime/src/org/argeo/cli/CommandsCli.java [new file with mode: 0644]
org.argeo.slc.runtime/src/org/argeo/cli/DescribedCommand.java [new file with mode: 0644]
org.argeo.slc.runtime/src/org/argeo/cli/HelpCommand.java [new file with mode: 0644]
org.argeo.slc.runtime/src/org/argeo/cli/fs/FileSync.java [new file with mode: 0644]
org.argeo.slc.runtime/src/org/argeo/cli/fs/FsCommands.java [new file with mode: 0644]
org.argeo.slc.runtime/src/org/argeo/cli/fs/PathSync.java [new file with mode: 0644]
org.argeo.slc.runtime/src/org/argeo/cli/fs/SyncFileVisitor.java [new file with mode: 0644]
org.argeo.slc.runtime/src/org/argeo/cli/fs/package-info.java [new file with mode: 0644]
org.argeo.slc.runtime/src/org/argeo/cli/package-info.java [new file with mode: 0644]
org.argeo.slc.runtime/src/org/argeo/cli/posix/Echo.java [new file with mode: 0644]
org.argeo.slc.runtime/src/org/argeo/cli/posix/PosixCommands.java [new file with mode: 0644]
org.argeo.slc.runtime/src/org/argeo/cli/posix/package-info.java [new file with mode: 0644]
org.argeo.slc.runtime/src/org/argeo/fs/BasicSyncFileVisitor.java [new file with mode: 0644]
org.argeo.slc.runtime/src/org/argeo/fs/FsUtils.java [new file with mode: 0644]
org.argeo.slc.runtime/src/org/argeo/fs/package-info.java [new file with mode: 0644]
org.argeo.slc.runtime/src/org/argeo/slc/runtime/ArgeoCli.java [new file with mode: 0644]
org.argeo.slc.runtime/src/org/argeo/sync/SyncException.java [new file with mode: 0644]
org.argeo.slc.runtime/src/org/argeo/sync/SyncResult.java [new file with mode: 0644]
org.argeo.slc.runtime/src/org/argeo/sync/package-info.java [new file with mode: 0644]

index b4ffde343bfb97e8e41d41c71f5ec79b4de071dc..8c8a7d40c4fd5a917630d1023187562adb9a357b 100644 (file)
                        <artifactId>org.eclipse.jgit</artifactId>
                </dependency>
 
-               <!-- Misc -->
+               <!-- Apache Commons -->
+               <dependency>
+                       <groupId>org.argeo.tp.apache.commons</groupId>
+                       <artifactId>org.apache.commons.exec</artifactId>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.apache.commons</groupId>
+                       <artifactId>org.apache.commons.cli</artifactId>
+               </dependency>
                <dependency>
                        <groupId>org.argeo.tp.apache.commons</groupId>
                        <artifactId>org.apache.commons.vfs</artifactId>
index ac978aac7d0908e14a7cc0b454472d240d65451c..a188f909da36c3ea9e79833e7af7bd30644d304e 100644 (file)
                </dependency>
 
                <!-- Argeo Commons UI -->
+<!--           <dependency> -->
+<!--                   <groupId>org.argeo.commons</groupId> -->
+<!--                   <artifactId>org.argeo.eclipse.ui</artifactId> -->
+<!--                   <version>${version.argeo-commons}</version> -->
+<!--           </dependency> -->
                <dependency>
                        <groupId>org.argeo.commons</groupId>
-                       <artifactId>org.argeo.eclipse.ui</artifactId>
+                       <artifactId>org.argeo.swt.specific.rap</artifactId>
                        <version>${version.argeo-commons}</version>
                </dependency>
                <dependency>
                        <groupId>org.argeo.commons</groupId>
-                       <artifactId>org.argeo.eclipse.ui.rap</artifactId>
+                       <artifactId>org.argeo.cms.swt</artifactId>
                        <version>${version.argeo-commons}</version>
                </dependency>
                <dependency>
                        <artifactId>org.argeo.cms.ui</artifactId>
                        <version>${version.argeo-commons}</version>
                </dependency>
-               <dependency>
-                       <groupId>org.argeo.commons</groupId>
-                       <artifactId>org.argeo.cms.ui.theme</artifactId>
-                       <version>${version.argeo-commons}</version>
-               </dependency>
+<!--           <dependency> -->
+<!--                   <groupId>org.argeo.commons</groupId> -->
+<!--                   <artifactId>org.argeo.cms.ui.theme</artifactId> -->
+<!--                   <version>${version.argeo-commons}</version> -->
+<!--           </dependency> -->
 
                <!-- Eclipse 3 specific -->
                <dependency>
index 52b07ea49c72b26254494b83f534f198220c88e3..1708a023980ab1cc884203f63d52b0f382ffbc30 100644 (file)
@@ -19,7 +19,7 @@
                <!-- RAP specific -->
                <dependency>
                        <groupId>org.argeo.commons</groupId>
-                       <artifactId>org.argeo.eclipse.ui.rap</artifactId>
+                       <artifactId>org.argeo.swt.specific.rap</artifactId>
                        <version>${version.argeo-commons}</version>
                </dependency>
 
index eaed2ec046a4e2599dbc6808ac577506cbdfb2bb..0bcfa6c79dfbb6e1a941d5dcfa949070e9ba4482 100644 (file)
@@ -19,7 +19,7 @@
 
                <dependency>
                        <groupId>org.argeo.commons</groupId>
-                       <artifactId>org.argeo.eclipse.ui.rap</artifactId>
+                       <artifactId>org.argeo.swt.specific.rap</artifactId>
                        <version>${version.argeo-commons}</version>
                        <scope>provided</scope>
                </dependency>
index 636fdd270b2db7cd72427018402b1c47ae76cb29..adec3012fea90c6eebd18950232ca3479b4482d0 100644 (file)
@@ -1,7 +1,7 @@
 package org.argeo.cms.ui.workbench.internal.jcr.commands;
 
-import static org.argeo.eclipse.ui.util.SingleSourcingConstants.FILE_SCHEME;
-import static org.argeo.eclipse.ui.util.SingleSourcingConstants.SCHEME_HOST_SEPARATOR;
+import static org.argeo.cms.ui.workbench.legacy.rap.SingleSourcingConstants.FILE_SCHEME;
+import static org.argeo.cms.ui.workbench.legacy.rap.SingleSourcingConstants.SCHEME_HOST_SEPARATOR;
 
 import java.io.File;
 import java.io.FileOutputStream;
@@ -18,9 +18,9 @@ import javax.jcr.RepositoryException;
 
 import org.argeo.cms.ui.jcr.model.SingleJcrNodeElem;
 import org.argeo.cms.ui.workbench.WorkbenchUiPlugin;
+import org.argeo.cms.ui.workbench.legacy.rap.OpenFile;
 import org.argeo.cms.ui.workbench.util.CommandUtils;
 import org.argeo.eclipse.ui.EclipseUiException;
-import org.argeo.eclipse.ui.specific.OpenFile;
 import org.argeo.jcr.JcrUtils;
 import org.eclipse.core.commands.AbstractHandler;
 import org.eclipse.core.commands.ExecutionEvent;
index 37feeb7c3547adb2cf2e560846c584f7ab4576db..50708b0b3c0ec672d202bd3fca9b8c5ad8dec633 100644 (file)
@@ -22,10 +22,10 @@ import org.apache.commons.io.IOUtils;
 import org.argeo.cms.ui.jcr.JcrDClickListener;
 import org.argeo.cms.ui.workbench.WorkbenchUiPlugin;
 import org.argeo.cms.ui.workbench.internal.jcr.parts.GenericNodeEditorInput;
+import org.argeo.cms.ui.workbench.legacy.rap.OpenFile;
+import org.argeo.cms.ui.workbench.legacy.rap.SingleSourcingException;
 import org.argeo.cms.ui.workbench.util.CommandUtils;
 import org.argeo.eclipse.ui.EclipseUiException;
-import org.argeo.eclipse.ui.specific.OpenFile;
-import org.argeo.eclipse.ui.specific.SingleSourcingException;
 import org.argeo.jcr.JcrUtils;
 import org.eclipse.jface.viewers.TreeViewer;
 
diff --git a/legacy/argeo-commons/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/legacy/rap/OpenFile.java b/legacy/argeo-commons/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/legacy/rap/OpenFile.java
new file mode 100644 (file)
index 0000000..4fe8508
--- /dev/null
@@ -0,0 +1,82 @@
+package org.argeo.cms.ui.workbench.legacy.rap;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.eclipse.core.commands.AbstractHandler;
+import org.eclipse.core.commands.ExecutionEvent;
+import org.eclipse.rap.rwt.RWT;
+import org.eclipse.rap.rwt.client.service.UrlLauncher;
+
+/**
+ * RWT specific object to open a file retrieved from the server. It forwards the
+ * request to the correct service after encoding file name and path in the
+ * request URI.
+ * 
+ * <p>
+ * The parameter "URI" is used to determine the correct file service, the path
+ * and the file name. An optional file name can be added to present the end user
+ * with a different file name as the one used to retrieve it.
+ * </p>
+ * 
+ * 
+ * <p>
+ * The instance specific service is called by its ID and must have been
+ * externally created
+ * </p>
+ */
+public class OpenFile extends AbstractHandler {
+       private final static Log log = LogFactory.getLog(OpenFile.class);
+
+       public final static String ID = SingleSourcingConstants.OPEN_FILE_CMD_ID;
+       public final static String PARAM_FILE_NAME = SingleSourcingConstants.PARAM_FILE_NAME;
+       public final static String PARAM_FILE_URI = SingleSourcingConstants.PARAM_FILE_URI;;
+
+       /* DEPENDENCY INJECTION */
+       private String openFileServiceId;
+
+       public Object execute(ExecutionEvent event) {
+               String fileName = event.getParameter(PARAM_FILE_NAME);
+               String fileUri = event.getParameter(PARAM_FILE_URI);
+               // Sanity check
+               if (fileUri == null || "".equals(fileUri.trim()) || openFileServiceId == null
+                               || "".equals(openFileServiceId.trim()))
+                       return null;
+
+               org.argeo.cms.ui.workbench.legacy.rap.OpenFile openFileClient = new org.argeo.cms.ui.workbench.legacy.rap.OpenFile();
+               openFileClient.execute(openFileServiceId, fileUri, fileName);
+               return null;
+       }
+
+       public Object execute(String openFileServiceId, String fileUri, String fileName) {
+               StringBuilder url = new StringBuilder();
+               url.append(RWT.getServiceManager().getServiceHandlerUrl(openFileServiceId));
+
+               if (notEmpty(fileName))
+                       url.append("&").append(SingleSourcingConstants.PARAM_FILE_NAME).append("=").append(fileName);
+               url.append("&").append(SingleSourcingConstants.PARAM_FILE_URI).append("=").append(fileUri);
+
+               String downloadUrl = url.toString();
+               if (log.isTraceEnabled())
+                       log.trace("Calling OpenFileService with ID: " + openFileServiceId + " , with download URL: " + downloadUrl);
+
+               UrlLauncher launcher = RWT.getClient().getService(UrlLauncher.class);
+               launcher.openURL(downloadUrl);
+               return null;
+       }
+
+       /* DEPENDENCY INJECTION */
+       public void setOpenFileServiceId(String openFileServiceId) {
+               this.openFileServiceId = openFileServiceId;
+       }
+
+       /** Simply checks if a string is not null nor empty */
+       public static boolean notEmpty(String stringToTest) {
+               return !(stringToTest == null || "".equals(stringToTest.trim()));
+       }
+
+       /** Simply checks if a string is null or empty */
+       public static boolean isEmpty(String stringToTest) {
+               return stringToTest == null || "".equals(stringToTest.trim());
+       }
+
+}
diff --git a/legacy/argeo-commons/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/legacy/rap/OpenFileService.java b/legacy/argeo-commons/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/legacy/rap/OpenFileService.java
new file mode 100644 (file)
index 0000000..5ca9c86
--- /dev/null
@@ -0,0 +1,94 @@
+package org.argeo.cms.ui.workbench.legacy.rap;
+
+import static org.argeo.cms.ui.workbench.legacy.rap.SingleSourcingConstants.FILE_SCHEME;
+import static org.argeo.cms.ui.workbench.legacy.rap.SingleSourcingConstants.SCHEME_HOST_SEPARATOR;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.eclipse.rap.rwt.service.ServiceHandler;
+
+/**
+ * RWT specific Basic Default service handler that retrieves a file on the
+ * server file system using its absolute path and forwards it to the end user
+ * browser.
+ * 
+ * Clients might extend to provide context specific services
+ */
+public class OpenFileService implements ServiceHandler {
+       public OpenFileService() {
+       }
+
+       public void service(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
+               String fileName = request.getParameter(SingleSourcingConstants.PARAM_FILE_NAME);
+               String uri = request.getParameter(SingleSourcingConstants.PARAM_FILE_URI);
+
+               // Use buffered array to directly write the stream?
+               if (!uri.startsWith(SingleSourcingConstants.FILE_SCHEME))
+                       throw new IllegalArgumentException(
+                                       "Open file service can only handle files that are on the server file system");
+
+               // Set the Metadata
+               response.setContentLength((int) getFileSize(uri));
+               if (OpenFile.isEmpty(fileName))
+                       fileName = getFileName(uri);
+               response.setContentType(getMimeType(uri, fileName));
+               String contentDisposition = "attachment; filename=\"" + fileName + "\"";
+               response.setHeader("Content-Disposition", contentDisposition);
+
+               // Useless for current use
+               // response.setHeader("Content-Transfer-Encoding", "binary");
+               // response.setHeader("Pragma", "no-cache");
+               // response.setHeader("Cache-Control", "no-cache, must-revalidate");
+
+               Path path = Paths.get(getAbsPathFromUri(uri));
+               Files.copy(path, response.getOutputStream());
+
+               // FIXME we always use temporary files for the time being.
+               // the deleteOnClose file only works when the JVM is closed so we
+               // explicitly delete to avoid overloading the server
+               if (path.startsWith("/tmp"))
+                       path.toFile().delete();
+       }
+
+       protected long getFileSize(String uri) throws IOException {
+               if (uri.startsWith(SingleSourcingConstants.FILE_SCHEME)) {
+                       Path path = Paths.get(getAbsPathFromUri(uri));
+                       return Files.size(path);
+               }
+               return -1l;
+       }
+
+       protected String getFileName(String uri) {
+               if (uri.startsWith(SingleSourcingConstants.FILE_SCHEME)) {
+                       Path path = Paths.get(getAbsPathFromUri(uri));
+                       return path.getFileName().toString();
+               }
+               return null;
+       }
+
+       private String getAbsPathFromUri(String uri) {
+               if (uri.startsWith(FILE_SCHEME))
+                       return uri.substring((FILE_SCHEME + SCHEME_HOST_SEPARATOR).length());
+               // else if (uri.startsWith(JCR_SCHEME))
+               // return uri.substring((JCR_SCHEME + SCHEME_HOST_SEPARATOR).length());
+               else
+                       throw new IllegalArgumentException("Unknown URI prefix for" + uri);
+       }
+
+       protected String getMimeType(String uri, String fileName) throws IOException {
+               if (uri.startsWith(FILE_SCHEME)) {
+                       Path path = Paths.get(getAbsPathFromUri(uri));
+                       String mimeType = Files.probeContentType(path);
+                       if (OpenFile.notEmpty(mimeType))
+                               return mimeType;
+               }
+               return "application/octet-stream";
+       }
+}
diff --git a/legacy/argeo-commons/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/legacy/rap/SingleSourcingConstants.java b/legacy/argeo-commons/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/legacy/rap/SingleSourcingConstants.java
new file mode 100644 (file)
index 0000000..51d15a0
--- /dev/null
@@ -0,0 +1,17 @@
+package org.argeo.cms.ui.workbench.legacy.rap;
+
+/**
+ * Centralise constants that are used in both RAP and RCP specific code to avoid
+ * duplicated declaration
+ */
+public interface SingleSourcingConstants {
+
+       // Single sourced open file command
+       String OPEN_FILE_CMD_ID = "org.argeo.cms.ui.workbench.openFile";
+       String PARAM_FILE_NAME = "param.fileName";
+       String PARAM_FILE_URI = "param.fileURI";
+
+       String SCHEME_HOST_SEPARATOR = "://";
+       String FILE_SCHEME = "file";
+       String JCR_SCHEME = "jcr";
+}
diff --git a/legacy/argeo-commons/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/legacy/rap/SingleSourcingException.java b/legacy/argeo-commons/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/legacy/rap/SingleSourcingException.java
new file mode 100644 (file)
index 0000000..563e812
--- /dev/null
@@ -0,0 +1,15 @@
+package org.argeo.cms.ui.workbench.legacy.rap;
+
+/** Exception related to SWT/RWT single sourcing. */
+public class SingleSourcingException extends RuntimeException {
+       private static final long serialVersionUID = -727700418055348468L;
+
+       public SingleSourcingException(String message, Throwable cause) {
+               super(message, cause);
+       }
+
+       public SingleSourcingException(String message) {
+               super(message);
+       }
+
+}
index 81c5c8d611dd552f8d3c34adb4f0ee013d13321b..81598ed8b85312ffe1e26e0b44834e1831d4acba 100644 (file)
                <!-- Commons UI -->
                <dependency>
                        <groupId>org.argeo.commons</groupId>
-                       <artifactId>org.argeo.eclipse.ui</artifactId>
+                       <artifactId>org.argeo.cms.swt</artifactId>
                        <version>${version.argeo-commons}</version>
                </dependency>
+               <dependency>
+                       <groupId>org.argeo.commons</groupId>
+                       <artifactId>org.argeo.swt.specific.rap</artifactId>
+                       <version>${version.argeo-commons}</version>
+                       <scope>provided</scope>
+               </dependency>
                <dependency>
                        <groupId>org.argeo.slc.legacy.commons</groupId>
                        <artifactId>org.argeo.cms.ui.workbench</artifactId>
@@ -49,7 +55,7 @@
                <!-- Argeo Base dependencies -->
                <dependency>
                        <groupId>org.argeo.commons</groupId>
-                       <artifactId>org.argeo.core</artifactId>
+                       <artifactId>org.argeo.cms</artifactId>
                        <version>${version.argeo-commons}</version>
                </dependency>
 
index 6fe69c8ebb4ca182485bdd21dfde158367cf2414..be65f6959ee937fe3cce139b0473251801687e3d 100644 (file)
                </dependency>
                <dependency>
                        <groupId>org.argeo.commons</groupId>
-                       <artifactId>org.argeo.eclipse.ui</artifactId>
+                       <artifactId>org.argeo.cms.swt</artifactId>
                        <version>${version.argeo-commons}</version>
                </dependency>
                <dependency>
                        <groupId>org.argeo.commons</groupId>
-                       <artifactId>org.argeo.eclipse.ui.rap</artifactId>
+                       <artifactId>org.argeo.swt.specific.rap</artifactId>
                        <version>${version.argeo-commons}</version>
                        <scope>provided</scope>
                </dependency>
index c3bac59231e1e97b4dae2a2731b4a58b2bc301f8..28c73295f7e5efb7b2bd22933b71b929e2c28c6e 100644 (file)
@@ -1,4 +1,4 @@
-Require-Capability: cms.datamodel;filter:="(name=ldap)",\
- cms.datamodel;filter:="(name=argeo)"
+#Require-Capability: cms.datamodel;filter:="(name=ldap)",\
+# cms.datamodel;filter:="(name=argeo)"
 
 Provide-Capability: cms.datamodel;name=slc;cnd=/org/argeo/slc/slc.cnd
index 151d880ce386d97f379a6970addd760990e88259..03ecd958028d64acb2dd963ac535d35519cac96c 100644 (file)
@@ -1,4 +1,6 @@
 Import-Package: javax.jcr.nodetype,\
 javax.jcr.security,\
+org.apache.jackrabbit.api,\
+org.apache.jackrabbit.commons,\
 org.argeo.api,\
 *
\ No newline at end of file
index 34d2e4d2dad529ceaeb953bfcdb63c51d69ffed2..35295596a631ae140acfa7236e3891db223ac770 100644 (file)
@@ -2,3 +2,27 @@ source.. = src/
 output.. = bin/
 bin.includes = META-INF/,\
                .
+additional.bundles = org.junit,\
+                     org.hamcrest,\
+                     org.apache.jackrabbit.core,\
+                     javax.jcr,\
+                     org.apache.jackrabbit.api,\
+                     org.apache.jackrabbit.data,\
+                     org.apache.jackrabbit.jcr.commons,\
+                     org.apache.jackrabbit.spi,\
+                     org.apache.jackrabbit.spi.commons,\
+                     org.slf4j.api,\
+                     org.slf4j.log4j12,\
+                     org.apache.log4j,\
+                     org.apache.commons.collections,\
+                     EDU.oswego.cs.dl.util.concurrent,\
+                     org.apache.lucene,\
+                     org.apache.tika.core,\
+                     org.apache.commons.dbcp,\
+                     org.apache.commons.pool,\
+                     com.google.guava,\
+                     org.apache.jackrabbit.jcr2spi,\
+                     org.apache.jackrabbit.spi2dav,\
+                     org.apache.httpcomponents.httpclient,\
+                     org.apache.httpcomponents.httpcore,\
+                     org.apache.tika.parsers
diff --git a/org.argeo.slc.jcr/src/org/argeo/cli/jcr/JcrCommands.java b/org.argeo.slc.jcr/src/org/argeo/cli/jcr/JcrCommands.java
new file mode 100644 (file)
index 0000000..ea74674
--- /dev/null
@@ -0,0 +1,18 @@
+package org.argeo.cli.jcr;
+
+import org.argeo.cli.CommandsCli;
+
+/** File utilities. */
+public class JcrCommands extends CommandsCli {
+
+       public JcrCommands(String commandName) {
+               super(commandName);
+               addCommand("sync", new JcrSync());
+       }
+
+       @Override
+       public String getDescription() {
+               return "Utilities around remote and local JCR repositories";
+       }
+
+}
diff --git a/org.argeo.slc.jcr/src/org/argeo/cli/jcr/JcrSync.java b/org.argeo.slc.jcr/src/org/argeo/cli/jcr/JcrSync.java
new file mode 100644 (file)
index 0000000..401f447
--- /dev/null
@@ -0,0 +1,133 @@
+package org.argeo.cli.jcr;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.nio.file.Paths;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import javax.jcr.Credentials;
+import javax.jcr.Node;
+import javax.jcr.Repository;
+import javax.jcr.RepositoryException;
+import javax.jcr.RepositoryFactory;
+import javax.jcr.Session;
+import javax.jcr.SimpleCredentials;
+
+import org.apache.commons.cli.CommandLine;
+import org.apache.commons.cli.Option;
+import org.apache.commons.cli.Options;
+import org.apache.jackrabbit.core.RepositoryImpl;
+import org.apache.jackrabbit.core.config.RepositoryConfig;
+import org.argeo.cli.CommandArgsException;
+import org.argeo.cli.CommandRuntimeException;
+import org.argeo.cli.DescribedCommand;
+import org.argeo.jackrabbit.client.ClientDavexRepositoryFactory;
+import org.argeo.jcr.JcrUtils;
+import org.argeo.sync.SyncResult;
+
+public class JcrSync implements DescribedCommand<SyncResult<Node>> {
+       public final static String DEFAULT_LOCALFS_CONFIG = "repository-localfs.xml";
+
+       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<Node> 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());
+
+                       // TODO make it configurable
+                       String sourceWorkspace = "home";
+                       String targetWorkspace = sourceWorkspace;
+
+                       final Repository sourceRepository;
+                       final Session sourceSession;
+                       Credentials sourceCredentials = null;
+                       final Repository targetRepository;
+                       final Session targetSession;
+                       Credentials targetCredentials = null;
+
+                       if ("http".equals(sourceUri.getScheme()) || "https".equals(sourceUri.getScheme())) {
+                               sourceRepository = createRemoteRepository(sourceUri);
+                       } else if (null == sourceUri.getScheme() || "file".equals(sourceUri.getScheme())) {
+                               RepositoryConfig repositoryConfig = RepositoryConfig.create(
+                                               JcrSync.class.getResourceAsStream(DEFAULT_LOCALFS_CONFIG), sourceUri.getPath().toString());
+                               sourceRepository = RepositoryImpl.create(repositoryConfig);
+                               sourceCredentials = new SimpleCredentials("admin", "admin".toCharArray());
+                       } else {
+                               throw new IllegalArgumentException("Unsupported scheme " + sourceUri.getScheme());
+                       }
+                       sourceSession = JcrUtils.loginOrCreateWorkspace(sourceRepository, sourceWorkspace, sourceCredentials);
+
+                       if ("http".equals(targetUri.getScheme()) || "https".equals(targetUri.getScheme())) {
+                               targetRepository = createRemoteRepository(targetUri);
+                       } else if (null == targetUri.getScheme() || "file".equals(targetUri.getScheme())) {
+                               RepositoryConfig repositoryConfig = RepositoryConfig.create(
+                                               JcrSync.class.getResourceAsStream(DEFAULT_LOCALFS_CONFIG), targetUri.getPath().toString());
+                               targetRepository = RepositoryImpl.create(repositoryConfig);
+                               targetCredentials = new SimpleCredentials("admin", "admin".toCharArray());
+                       } else {
+                               throw new IllegalArgumentException("Unsupported scheme " + targetUri.getScheme());
+                       }
+                       targetSession = JcrUtils.loginOrCreateWorkspace(targetRepository, targetWorkspace, targetCredentials);
+
+                       JcrUtils.copy(sourceSession.getRootNode(), targetSession.getRootNode());
+                       return new SyncResult<Node>();
+               } catch (URISyntaxException e) {
+                       throw new CommandArgsException(e);
+               } catch (Exception e) {
+                       throw new CommandRuntimeException(e, this, t);
+               }
+       }
+
+       protected Repository createRemoteRepository(URI uri) throws RepositoryException {
+               RepositoryFactory repositoryFactory = new ClientDavexRepositoryFactory();
+               Map<String, String> params = new HashMap<String, String>();
+               params.put(ClientDavexRepositoryFactory.JACKRABBIT_DAVEX_URI, uri.toString());
+               // FIXME make it configurable
+               params.put(ClientDavexRepositoryFactory.JACKRABBIT_REMOTE_DEFAULT_WORKSPACE, "sys");
+               return repositoryFactory.getRepository(params);
+       }
+
+       @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 JcrSync(), args);
+       }
+
+       @Override
+       public String getDescription() {
+               return "Synchronises JCR repositories";
+       }
+
+}
diff --git a/org.argeo.slc.jcr/src/org/argeo/cli/jcr/package-info.java b/org.argeo.slc.jcr/src/org/argeo/cli/jcr/package-info.java
new file mode 100644 (file)
index 0000000..6f3f01f
--- /dev/null
@@ -0,0 +1,2 @@
+/** JCR CLI commands. */
+package org.argeo.cli.jcr;
\ No newline at end of file
diff --git a/org.argeo.slc.jcr/src/org/argeo/cli/jcr/repository-localfs.xml b/org.argeo.slc.jcr/src/org/argeo/cli/jcr/repository-localfs.xml
new file mode 100644 (file)
index 0000000..5e7759c
--- /dev/null
@@ -0,0 +1,76 @@
+<?xml version="1.0"?>
+<!--
+
+    Copyright (C) 2007-2012 Argeo GmbH
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+            http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+-->
+<!DOCTYPE Repository PUBLIC "-//The Apache Software Foundation//DTD Jackrabbit 1.6//EN"
+                            "http://jackrabbit.apache.org/dtd/repository-2.0.dtd">
+<Repository>
+       <!-- File system and datastore -->
+       <FileSystem class="org.apache.jackrabbit.core.fs.local.LocalFileSystem">
+               <param name="path" value="${rep.home}/repository" />
+       </FileSystem>
+       <DataStore class="org.apache.jackrabbit.core.data.FileDataStore">
+               <param name="path" value="${rep.home}/datastore" />
+       </DataStore>
+
+       <!-- Workspace templates -->
+       <Workspaces rootPath="${rep.home}/workspaces"
+               defaultWorkspace="main" configRootPath="/workspaces" />
+       <Workspace name="${wsp.name}">
+               <FileSystem class="org.apache.jackrabbit.core.fs.local.LocalFileSystem">
+                       <param name="path" value="${wsp.home}" />
+               </FileSystem>
+               <PersistenceManager
+                       class="org.apache.jackrabbit.core.persistence.bundle.BundleFsPersistenceManager">
+                       <param name="blobFSBlockSize" value="1" />
+               </PersistenceManager>
+               <SearchIndex class="org.apache.jackrabbit.core.query.lucene.SearchIndex">
+                       <param name="path" value="${rep.home}/repository/index" />
+               </SearchIndex>
+       </Workspace>
+
+       <!-- Versioning -->
+       <Versioning rootPath="${rep.home}/version">
+               <FileSystem class="org.apache.jackrabbit.core.fs.local.LocalFileSystem">
+                       <param name="path" value="${rep.home}/version" />
+               </FileSystem>
+               <PersistenceManager
+                       class="org.apache.jackrabbit.core.persistence.bundle.BundleFsPersistenceManager">
+                       <param name="blobFSBlockSize" value="1" />
+               </PersistenceManager>
+       </Versioning>
+
+       <!-- Indexing -->
+       <SearchIndex class="org.apache.jackrabbit.core.query.lucene.SearchIndex">
+               <param name="path" value="${rep.home}/repository/index" />
+               <param name="tikaConfigPath" value="tika-config.xml"/>
+       </SearchIndex>
+
+       <!-- Security -->
+       <Security appName="Jackrabbit">
+               <SecurityManager
+                       class="org.apache.jackrabbit.core.security.simple.SimpleSecurityManager"
+                       workspaceName="security" />
+               <AccessManager
+                       class="org.apache.jackrabbit.core.security.simple.SimpleAccessManager" />
+               <LoginModule
+                       class="org.apache.jackrabbit.core.security.simple.SimpleLoginModule">
+                       <param name="anonymousId" value="anonymous" />
+                       <param name="adminId" value="admin" />
+               </LoginModule>
+       </Security>
+</Repository>
\ No newline at end of file
index e801ebfb4680123285c15553dc70584276fe0057..20cad808ae9056dcfc70d1dfeb9a41c2633be20a 100644 (file)
@@ -1,7 +1,12 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <classpath>
-       <classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-11"/>
+       <classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-11">
+               <attributes>
+                       <attribute name="module" value="true"/>
+               </attributes>
+       </classpathentry>
        <classpathentry kind="con" path="org.eclipse.pde.core.requiredPlugins"/>
        <classpathentry kind="src" path="src"/>
+       <classpathentry kind="src" path="ext/test"/>
        <classpathentry kind="output" path="bin"/>
 </classpath>
diff --git a/org.argeo.slc.runtime/ext/test/org/argeo/fs/FsUtilsTest.java b/org.argeo.slc.runtime/ext/test/org/argeo/fs/FsUtilsTest.java
new file mode 100644 (file)
index 0000000..793216b
--- /dev/null
@@ -0,0 +1,49 @@
+package org.argeo.fs;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+
+/** {@link FsUtils} tests. */
+public class FsUtilsTest {
+       final static String FILE00 = "file00";
+       final static String FILE01 = "file01";
+       final static String SUB_DIR = "subDir";
+
+       public void testDelete() throws IOException {
+               Path dir = createDir00();
+               assert Files.exists(dir);
+               FsUtils.delete(dir);
+               assert !Files.exists(dir);
+       }
+
+       public void testSync() throws IOException {
+               Path source = createDir00();
+               Path target = Files.createTempDirectory(getClass().getName());
+               FsUtils.sync(source, target);
+               assert Files.exists(target.resolve(FILE00));
+               assert Files.exists(target.resolve(SUB_DIR));
+               assert Files.exists(target.resolve(SUB_DIR + File.separator + FILE01));
+               FsUtils.delete(source.resolve(SUB_DIR));
+               FsUtils.sync(source, target, true);
+               assert Files.exists(target.resolve(FILE00));
+               assert !Files.exists(target.resolve(SUB_DIR));
+               assert !Files.exists(target.resolve(SUB_DIR + File.separator + FILE01));
+
+               // clean up
+               FsUtils.delete(source);
+               FsUtils.delete(target);
+
+       }
+
+       Path createDir00() throws IOException {
+               Path base = Files.createTempDirectory(getClass().getName());
+               base.toFile().deleteOnExit();
+               Files.createFile(base.resolve(FILE00)).toFile().deleteOnExit();
+               Path subDir = Files.createDirectories(base.resolve(SUB_DIR));
+               subDir.toFile().deleteOnExit();
+               Files.createFile(subDir.resolve(FILE01)).toFile().deleteOnExit();
+               return base;
+       }
+}
diff --git a/org.argeo.slc.runtime/src/org/argeo/cli/CommandArgsException.java b/org.argeo.slc.runtime/src/org/argeo/cli/CommandArgsException.java
new file mode 100644 (file)
index 0000000..d7a615a
--- /dev/null
@@ -0,0 +1,32 @@
+package org.argeo.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.slc.runtime/src/org/argeo/cli/CommandRuntimeException.java b/org.argeo.slc.runtime/src/org/argeo/cli/CommandRuntimeException.java
new file mode 100644 (file)
index 0000000..68b9a18
--- /dev/null
@@ -0,0 +1,35 @@
+package org.argeo.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.slc.runtime/src/org/argeo/cli/CommandsCli.java b/org.argeo.slc.runtime/src/org/argeo/cli/CommandsCli.java
new file mode 100644 (file)
index 0000000..b0879f0
--- /dev/null
@@ -0,0 +1,131 @@
+package org.argeo.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.slc.runtime/src/org/argeo/cli/DescribedCommand.java b/org.argeo.slc.runtime/src/org/argeo/cli/DescribedCommand.java
new file mode 100644 (file)
index 0000000..9587206
--- /dev/null
@@ -0,0 +1,55 @@
+package org.argeo.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.slc.runtime/src/org/argeo/cli/HelpCommand.java b/org.argeo.slc.runtime/src/org/argeo/cli/HelpCommand.java
new file mode 100644 (file)
index 0000000..755ce59
--- /dev/null
@@ -0,0 +1,143 @@
+package org.argeo.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.slc.runtime/src/org/argeo/cli/fs/FileSync.java b/org.argeo.slc.runtime/src/org/argeo/cli/fs/FileSync.java
new file mode 100644 (file)
index 0000000..ba529ea
--- /dev/null
@@ -0,0 +1,102 @@
+package org.argeo.cli.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.cli.CommandArgsException;
+import org.argeo.cli.DescribedCommand;
+import org.argeo.sync.SyncResult;
+
+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.slc.runtime/src/org/argeo/cli/fs/FsCommands.java b/org.argeo.slc.runtime/src/org/argeo/cli/fs/FsCommands.java
new file mode 100644 (file)
index 0000000..c08ad00
--- /dev/null
@@ -0,0 +1,18 @@
+package org.argeo.cli.fs;
+
+import org.argeo.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.slc.runtime/src/org/argeo/cli/fs/PathSync.java b/org.argeo.slc.runtime/src/org/argeo/cli/fs/PathSync.java
new file mode 100644 (file)
index 0000000..9ab9caf
--- /dev/null
@@ -0,0 +1,61 @@
+package org.argeo.cli.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.sync.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.slc.runtime/src/org/argeo/cli/fs/SyncFileVisitor.java b/org.argeo.slc.runtime/src/org/argeo/cli/fs/SyncFileVisitor.java
new file mode 100644 (file)
index 0000000..892df50
--- /dev/null
@@ -0,0 +1,31 @@
+package org.argeo.cli.fs;
+
+import java.nio.file.Path;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.argeo.fs.BasicSyncFileVisitor;
+
+/** Synchronises two directory structures. */
+public class SyncFileVisitor extends BasicSyncFileVisitor {
+       private final static Log log = LogFactory.getLog(SyncFileVisitor.class);
+
+       public SyncFileVisitor(Path sourceBasePath, Path targetBasePath, boolean delete, boolean recursive) {
+               super(sourceBasePath, targetBasePath, delete, recursive);
+       }
+
+       @Override
+       protected void error(Object obj, Throwable e) {
+               log.error(obj, e);
+       }
+
+       @Override
+       protected boolean isTraceEnabled() {
+               return log.isTraceEnabled();
+       }
+
+       @Override
+       protected void trace(Object obj) {
+               log.trace(obj);
+       }
+}
diff --git a/org.argeo.slc.runtime/src/org/argeo/cli/fs/package-info.java b/org.argeo.slc.runtime/src/org/argeo/cli/fs/package-info.java
new file mode 100644 (file)
index 0000000..8ad42b2
--- /dev/null
@@ -0,0 +1,2 @@
+/** File system CLI commands. */
+package org.argeo.cli.fs;
\ No newline at end of file
diff --git a/org.argeo.slc.runtime/src/org/argeo/cli/package-info.java b/org.argeo.slc.runtime/src/org/argeo/cli/package-info.java
new file mode 100644 (file)
index 0000000..2389593
--- /dev/null
@@ -0,0 +1,2 @@
+/** Command line API. */
+package org.argeo.cli;
\ No newline at end of file
diff --git a/org.argeo.slc.runtime/src/org/argeo/cli/posix/Echo.java b/org.argeo.slc.runtime/src/org/argeo/cli/posix/Echo.java
new file mode 100644 (file)
index 0000000..5746ebd
--- /dev/null
@@ -0,0 +1,46 @@
+package org.argeo.cli.posix;
+
+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.cli.DescribedCommand;
+
+public class Echo implements DescribedCommand<String> {
+
+       @Override
+       public Options getOptions() {
+               Options options = new Options();
+               options.addOption(Option.builder("n").desc("do not output the trailing newline").build());
+               return options;
+       }
+
+       @Override
+       public String getDescription() {
+               return "Display a line of text";
+       }
+
+       @Override
+       public String getUsage() {
+               return "[STRING]...";
+       }
+
+       @Override
+       public String apply(List<String> args) {
+               CommandLine cl = toCommandLine(args);
+
+               StringBuffer sb = new StringBuffer();
+               for (String s : cl.getArgList()) {
+                       sb.append(s).append(' ');
+               }
+
+               if (cl.hasOption('n')) {
+                       sb.deleteCharAt(sb.length() - 1);
+               } else {
+                       sb.setCharAt(sb.length() - 1, '\n');
+               }
+               return sb.toString();
+       }
+
+}
diff --git a/org.argeo.slc.runtime/src/org/argeo/cli/posix/PosixCommands.java b/org.argeo.slc.runtime/src/org/argeo/cli/posix/PosixCommands.java
new file mode 100644 (file)
index 0000000..bb6af67
--- /dev/null
@@ -0,0 +1,21 @@
+package org.argeo.cli.posix;
+
+import org.argeo.cli.CommandsCli;
+
+/** POSIX commands. */
+public class PosixCommands extends CommandsCli {
+
+       public PosixCommands(String commandName) {
+               super(commandName);
+               addCommand("echo", new Echo());
+       }
+
+       @Override
+       public String getDescription() {
+               return "Reimplementation of some POSIX commands in plain Java";
+       }
+
+       public static void main(String[] args) {
+               mainImpl(new PosixCommands("argeo-posix"), args);
+       }
+}
diff --git a/org.argeo.slc.runtime/src/org/argeo/cli/posix/package-info.java b/org.argeo.slc.runtime/src/org/argeo/cli/posix/package-info.java
new file mode 100644 (file)
index 0000000..b0d1a46
--- /dev/null
@@ -0,0 +1,2 @@
+/** Posix CLI commands. */
+package org.argeo.cli.posix;
\ No newline at end of file
diff --git a/org.argeo.slc.runtime/src/org/argeo/fs/BasicSyncFileVisitor.java b/org.argeo.slc.runtime/src/org/argeo/fs/BasicSyncFileVisitor.java
new file mode 100644 (file)
index 0000000..03bac59
--- /dev/null
@@ -0,0 +1,164 @@
+package org.argeo.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;
+
+import org.argeo.sync.SyncResult;
+
+/** 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 {
+                                               FsUtils.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.slc.runtime/src/org/argeo/fs/FsUtils.java b/org.argeo.slc.runtime/src/org/argeo/fs/FsUtils.java
new file mode 100644 (file)
index 0000000..c96f56e
--- /dev/null
@@ -0,0 +1,58 @@
+package org.argeo.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;
+
+/** Utilities around the standard Java file abstractions. */
+public class FsUtils {
+       /** 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. */
+       public static void delete(Path path) {
+               try {
+                       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 FsUtils() {
+       }
+
+}
diff --git a/org.argeo.slc.runtime/src/org/argeo/fs/package-info.java b/org.argeo.slc.runtime/src/org/argeo/fs/package-info.java
new file mode 100644 (file)
index 0000000..ea2de9e
--- /dev/null
@@ -0,0 +1,2 @@
+/** Generic file system utilities. */
+package org.argeo.fs;
\ No newline at end of file
diff --git a/org.argeo.slc.runtime/src/org/argeo/slc/runtime/ArgeoCli.java b/org.argeo.slc.runtime/src/org/argeo/slc/runtime/ArgeoCli.java
new file mode 100644 (file)
index 0000000..21cca75
--- /dev/null
@@ -0,0 +1,32 @@
+package org.argeo.slc.runtime;
+
+import org.apache.commons.cli.Option;
+import org.argeo.cli.CommandsCli;
+import org.argeo.cli.fs.FsCommands;
+import org.argeo.cli.posix.PosixCommands;
+
+/** Argeo command line tools. */
+public class ArgeoCli extends CommandsCli {
+
+       public ArgeoCli(String commandName) {
+               super(commandName);
+               // Common options
+               options.addOption(Option.builder("v").hasArg().argName("verbose").desc("verbosity").build());
+               options.addOption(
+                               Option.builder("D").hasArgs().argName("property=value").desc("use value for given property").build());
+
+               addCommandsCli(new PosixCommands("posix"));
+               addCommandsCli(new FsCommands("fs"));
+//             addCommandsCli(new JcrCommands("jcr"));
+       }
+
+       @Override
+       public String getDescription() {
+               return "Argeo command line utilities";
+       }
+
+       public static void main(String[] args) {
+               mainImpl(new ArgeoCli("argeo"), args);
+       }
+
+}
diff --git a/org.argeo.slc.runtime/src/org/argeo/sync/SyncException.java b/org.argeo.slc.runtime/src/org/argeo/sync/SyncException.java
new file mode 100644 (file)
index 0000000..89bf869
--- /dev/null
@@ -0,0 +1,18 @@
+package org.argeo.sync;
+
+/** Commons exception for sync */
+public class SyncException extends RuntimeException {
+       private static final long serialVersionUID = -3371314343580218538L;
+
+       public SyncException(String message) {
+               super(message);
+       }
+
+       public SyncException(String message, Throwable cause) {
+               super(message, cause);
+       }
+
+       public SyncException(Object source, Object target, Throwable cause) {
+               super("Cannot sync from " + source + " to " + target, cause);
+       }
+}
diff --git a/org.argeo.slc.runtime/src/org/argeo/sync/SyncResult.java b/org.argeo.slc.runtime/src/org/argeo/sync/SyncResult.java
new file mode 100644 (file)
index 0000000..6d12ada
--- /dev/null
@@ -0,0 +1,101 @@
+package org.argeo.sync;
+
+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.slc.runtime/src/org/argeo/sync/package-info.java b/org.argeo.slc.runtime/src/org/argeo/sync/package-info.java
new file mode 100644 (file)
index 0000000..c5e9da0
--- /dev/null
@@ -0,0 +1,2 @@
+/** Synchrnoisation related utilities. */
+package org.argeo.sync;
\ No newline at end of file