From f27e8fc8c485b839950728871671605b8666770e Mon Sep 17 00:00:00 2001 From: Mathieu Baudier Date: Thu, 9 Dec 2021 12:11:59 +0100 Subject: [PATCH] Move CLI utilities from Argeo Commons --- dep/org.argeo.slc.dep.minimal/pom.xml | 10 +- .../dep/org.argeo.dep.cms.platform/pom.xml | 19 +- .../org.argeo.cms.ui.workbench.rap/pom.xml | 2 +- .../org.argeo.cms.ui.workbench/pom.xml | 2 +- .../internal/jcr/commands/DumpNode.java | 6 +- .../jcr/WorkbenchJcrDClickListener.java | 4 +- .../cms/ui/workbench/legacy/rap/OpenFile.java | 82 +++++++++ .../workbench/legacy/rap/OpenFileService.java | 94 ++++++++++ .../legacy/rap/SingleSourcingConstants.java | 17 ++ .../legacy/rap/SingleSourcingException.java | 15 ++ legacy/org.argeo.slc.client.ui.dist/pom.xml | 10 +- legacy/org.argeo.slc.client.ui/pom.xml | 4 +- org.argeo.slc.api/bnd.bnd | 4 +- org.argeo.slc.jcr/bnd.bnd | 2 + org.argeo.slc.jcr/build.properties | 24 +++ .../src/org/argeo/cli/jcr/JcrCommands.java | 18 ++ .../src/org/argeo/cli/jcr/JcrSync.java | 133 ++++++++++++++ .../src/org/argeo/cli/jcr/package-info.java | 2 + .../org/argeo/cli/jcr/repository-localfs.xml | 76 ++++++++ org.argeo.slc.runtime/.classpath | 7 +- .../ext/test/org/argeo/fs/FsUtilsTest.java | 49 ++++++ .../org/argeo/cli/CommandArgsException.java | 32 ++++ .../argeo/cli/CommandRuntimeException.java | 35 ++++ .../src/org/argeo/cli/CommandsCli.java | 131 ++++++++++++++ .../src/org/argeo/cli/DescribedCommand.java | 55 ++++++ .../src/org/argeo/cli/HelpCommand.java | 143 +++++++++++++++ .../src/org/argeo/cli/fs/FileSync.java | 102 +++++++++++ .../src/org/argeo/cli/fs/FsCommands.java | 18 ++ .../src/org/argeo/cli/fs/PathSync.java | 61 +++++++ .../src/org/argeo/cli/fs/SyncFileVisitor.java | 31 ++++ .../src/org/argeo/cli/fs/package-info.java | 2 + .../src/org/argeo/cli/package-info.java | 2 + .../src/org/argeo/cli/posix/Echo.java | 46 +++++ .../org/argeo/cli/posix/PosixCommands.java | 21 +++ .../src/org/argeo/cli/posix/package-info.java | 2 + .../org/argeo/fs/BasicSyncFileVisitor.java | 164 ++++++++++++++++++ .../src/org/argeo/fs/FsUtils.java | 58 +++++++ .../src/org/argeo/fs/package-info.java | 2 + .../src/org/argeo/slc/runtime/ArgeoCli.java | 32 ++++ .../src/org/argeo/sync/SyncException.java | 18 ++ .../src/org/argeo/sync/SyncResult.java | 101 +++++++++++ .../src/org/argeo/sync/package-info.java | 2 + 42 files changed, 1616 insertions(+), 22 deletions(-) create mode 100644 legacy/argeo-commons/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/legacy/rap/OpenFile.java create mode 100644 legacy/argeo-commons/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/legacy/rap/OpenFileService.java create mode 100644 legacy/argeo-commons/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/legacy/rap/SingleSourcingConstants.java create mode 100644 legacy/argeo-commons/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/legacy/rap/SingleSourcingException.java create mode 100644 org.argeo.slc.jcr/src/org/argeo/cli/jcr/JcrCommands.java create mode 100644 org.argeo.slc.jcr/src/org/argeo/cli/jcr/JcrSync.java create mode 100644 org.argeo.slc.jcr/src/org/argeo/cli/jcr/package-info.java create mode 100644 org.argeo.slc.jcr/src/org/argeo/cli/jcr/repository-localfs.xml create mode 100644 org.argeo.slc.runtime/ext/test/org/argeo/fs/FsUtilsTest.java create mode 100644 org.argeo.slc.runtime/src/org/argeo/cli/CommandArgsException.java create mode 100644 org.argeo.slc.runtime/src/org/argeo/cli/CommandRuntimeException.java create mode 100644 org.argeo.slc.runtime/src/org/argeo/cli/CommandsCli.java create mode 100644 org.argeo.slc.runtime/src/org/argeo/cli/DescribedCommand.java create mode 100644 org.argeo.slc.runtime/src/org/argeo/cli/HelpCommand.java create mode 100644 org.argeo.slc.runtime/src/org/argeo/cli/fs/FileSync.java create mode 100644 org.argeo.slc.runtime/src/org/argeo/cli/fs/FsCommands.java create mode 100644 org.argeo.slc.runtime/src/org/argeo/cli/fs/PathSync.java create mode 100644 org.argeo.slc.runtime/src/org/argeo/cli/fs/SyncFileVisitor.java create mode 100644 org.argeo.slc.runtime/src/org/argeo/cli/fs/package-info.java create mode 100644 org.argeo.slc.runtime/src/org/argeo/cli/package-info.java create mode 100644 org.argeo.slc.runtime/src/org/argeo/cli/posix/Echo.java create mode 100644 org.argeo.slc.runtime/src/org/argeo/cli/posix/PosixCommands.java create mode 100644 org.argeo.slc.runtime/src/org/argeo/cli/posix/package-info.java create mode 100644 org.argeo.slc.runtime/src/org/argeo/fs/BasicSyncFileVisitor.java create mode 100644 org.argeo.slc.runtime/src/org/argeo/fs/FsUtils.java create mode 100644 org.argeo.slc.runtime/src/org/argeo/fs/package-info.java create mode 100644 org.argeo.slc.runtime/src/org/argeo/slc/runtime/ArgeoCli.java create mode 100644 org.argeo.slc.runtime/src/org/argeo/sync/SyncException.java create mode 100644 org.argeo.slc.runtime/src/org/argeo/sync/SyncResult.java create mode 100644 org.argeo.slc.runtime/src/org/argeo/sync/package-info.java diff --git a/dep/org.argeo.slc.dep.minimal/pom.xml b/dep/org.argeo.slc.dep.minimal/pom.xml index b4ffde343..8c8a7d40c 100644 --- a/dep/org.argeo.slc.dep.minimal/pom.xml +++ b/dep/org.argeo.slc.dep.minimal/pom.xml @@ -104,7 +104,15 @@ org.eclipse.jgit - + + + org.argeo.tp.apache.commons + org.apache.commons.exec + + + org.argeo.tp.apache.commons + org.apache.commons.cli + org.argeo.tp.apache.commons org.apache.commons.vfs diff --git a/legacy/argeo-commons/dep/org.argeo.dep.cms.platform/pom.xml b/legacy/argeo-commons/dep/org.argeo.dep.cms.platform/pom.xml index ac978aac7..a188f909d 100644 --- a/legacy/argeo-commons/dep/org.argeo.dep.cms.platform/pom.xml +++ b/legacy/argeo-commons/dep/org.argeo.dep.cms.platform/pom.xml @@ -42,14 +42,19 @@ + + + + + org.argeo.commons - org.argeo.eclipse.ui + org.argeo.swt.specific.rap ${version.argeo-commons} org.argeo.commons - org.argeo.eclipse.ui.rap + org.argeo.cms.swt ${version.argeo-commons} @@ -57,11 +62,11 @@ org.argeo.cms.ui ${version.argeo-commons} - - org.argeo.commons - org.argeo.cms.ui.theme - ${version.argeo-commons} - + + + + + diff --git a/legacy/argeo-commons/org.argeo.cms.ui.workbench.rap/pom.xml b/legacy/argeo-commons/org.argeo.cms.ui.workbench.rap/pom.xml index 52b07ea49..1708a0239 100644 --- a/legacy/argeo-commons/org.argeo.cms.ui.workbench.rap/pom.xml +++ b/legacy/argeo-commons/org.argeo.cms.ui.workbench.rap/pom.xml @@ -19,7 +19,7 @@ org.argeo.commons - org.argeo.eclipse.ui.rap + org.argeo.swt.specific.rap ${version.argeo-commons} diff --git a/legacy/argeo-commons/org.argeo.cms.ui.workbench/pom.xml b/legacy/argeo-commons/org.argeo.cms.ui.workbench/pom.xml index eaed2ec04..0bcfa6c79 100644 --- a/legacy/argeo-commons/org.argeo.cms.ui.workbench/pom.xml +++ b/legacy/argeo-commons/org.argeo.cms.ui.workbench/pom.xml @@ -19,7 +19,7 @@ org.argeo.commons - org.argeo.eclipse.ui.rap + org.argeo.swt.specific.rap ${version.argeo-commons} provided diff --git a/legacy/argeo-commons/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/internal/jcr/commands/DumpNode.java b/legacy/argeo-commons/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/internal/jcr/commands/DumpNode.java index 636fdd270..adec3012f 100644 --- a/legacy/argeo-commons/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/internal/jcr/commands/DumpNode.java +++ b/legacy/argeo-commons/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/internal/jcr/commands/DumpNode.java @@ -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; diff --git a/legacy/argeo-commons/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/jcr/WorkbenchJcrDClickListener.java b/legacy/argeo-commons/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/jcr/WorkbenchJcrDClickListener.java index 37feeb7c3..50708b0b3 100644 --- a/legacy/argeo-commons/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/jcr/WorkbenchJcrDClickListener.java +++ b/legacy/argeo-commons/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/jcr/WorkbenchJcrDClickListener.java @@ -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 index 000000000..4fe850826 --- /dev/null +++ b/legacy/argeo-commons/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/legacy/rap/OpenFile.java @@ -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. + * + *

+ * 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. + *

+ * + * + *

+ * The instance specific service is called by its ID and must have been + * externally created + *

+ */ +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 index 000000000..5ca9c8698 --- /dev/null +++ b/legacy/argeo-commons/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/legacy/rap/OpenFileService.java @@ -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 index 000000000..51d15a071 --- /dev/null +++ b/legacy/argeo-commons/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/legacy/rap/SingleSourcingConstants.java @@ -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 index 000000000..563e81226 --- /dev/null +++ b/legacy/argeo-commons/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/legacy/rap/SingleSourcingException.java @@ -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); + } + +} diff --git a/legacy/org.argeo.slc.client.ui.dist/pom.xml b/legacy/org.argeo.slc.client.ui.dist/pom.xml index 81c5c8d61..81598ed8b 100644 --- a/legacy/org.argeo.slc.client.ui.dist/pom.xml +++ b/legacy/org.argeo.slc.client.ui.dist/pom.xml @@ -14,9 +14,15 @@ org.argeo.commons - org.argeo.eclipse.ui + org.argeo.cms.swt ${version.argeo-commons} + + org.argeo.commons + org.argeo.swt.specific.rap + ${version.argeo-commons} + provided + org.argeo.slc.legacy.commons org.argeo.cms.ui.workbench @@ -49,7 +55,7 @@ org.argeo.commons - org.argeo.core + org.argeo.cms ${version.argeo-commons} diff --git a/legacy/org.argeo.slc.client.ui/pom.xml b/legacy/org.argeo.slc.client.ui/pom.xml index 6fe69c8eb..be65f6959 100644 --- a/legacy/org.argeo.slc.client.ui/pom.xml +++ b/legacy/org.argeo.slc.client.ui/pom.xml @@ -25,12 +25,12 @@ org.argeo.commons - org.argeo.eclipse.ui + org.argeo.cms.swt ${version.argeo-commons} org.argeo.commons - org.argeo.eclipse.ui.rap + org.argeo.swt.specific.rap ${version.argeo-commons} provided diff --git a/org.argeo.slc.api/bnd.bnd b/org.argeo.slc.api/bnd.bnd index c3bac5923..28c73295f 100644 --- a/org.argeo.slc.api/bnd.bnd +++ b/org.argeo.slc.api/bnd.bnd @@ -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 diff --git a/org.argeo.slc.jcr/bnd.bnd b/org.argeo.slc.jcr/bnd.bnd index 151d880ce..03ecd9580 100644 --- a/org.argeo.slc.jcr/bnd.bnd +++ b/org.argeo.slc.jcr/bnd.bnd @@ -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 diff --git a/org.argeo.slc.jcr/build.properties b/org.argeo.slc.jcr/build.properties index 34d2e4d2d..35295596a 100644 --- a/org.argeo.slc.jcr/build.properties +++ b/org.argeo.slc.jcr/build.properties @@ -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 index 000000000..ea7467462 --- /dev/null +++ b/org.argeo.slc.jcr/src/org/argeo/cli/jcr/JcrCommands.java @@ -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 index 000000000..401f447c9 --- /dev/null +++ b/org.argeo.slc.jcr/src/org/argeo/cli/jcr/JcrSync.java @@ -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> { + 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 apply(List t) { + try { + CommandLine line = toCommandLine(t); + List remaining = line.getArgList(); + if (remaining.size() == 0) { + throw new CommandArgsException("There must be at least one argument"); + } + URI sourceUri = new URI(remaining.get(0)); + URI targetUri; + if (remaining.size() == 1) { + targetUri = Paths.get(System.getProperty("user.dir")).toUri(); + } else { + targetUri = new URI(remaining.get(1)); + } + boolean delete = line.hasOption(deleteOption.getLongOpt()); + boolean recursive = line.hasOption(recursiveOption.getLongOpt()); + + // 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(); + } 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 params = new HashMap(); + 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 index 000000000..6f3f01f3a --- /dev/null +++ b/org.argeo.slc.jcr/src/org/argeo/cli/jcr/package-info.java @@ -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 index 000000000..5e7759cf4 --- /dev/null +++ b/org.argeo.slc.jcr/src/org/argeo/cli/jcr/repository-localfs.xml @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/org.argeo.slc.runtime/.classpath b/org.argeo.slc.runtime/.classpath index e801ebfb4..20cad808a 100644 --- a/org.argeo.slc.runtime/.classpath +++ b/org.argeo.slc.runtime/.classpath @@ -1,7 +1,12 @@ - + + + + + + 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 index 000000000..793216b1d --- /dev/null +++ b/org.argeo.slc.runtime/ext/test/org/argeo/fs/FsUtilsTest.java @@ -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 index 000000000..d7a615a8e --- /dev/null +++ b/org.argeo.slc.runtime/src/org/argeo/cli/CommandArgsException.java @@ -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 index 000000000..68b9a18cf --- /dev/null +++ b/org.argeo.slc.runtime/src/org/argeo/cli/CommandRuntimeException.java @@ -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 arguments; + + public CommandRuntimeException(Throwable e, DescribedCommand command, List arguments) { + this(null, e, command, arguments); + } + + public CommandRuntimeException(String message, DescribedCommand command, List arguments) { + this(message, null, command, arguments); + } + + public CommandRuntimeException(String message, Throwable e, DescribedCommand command, List arguments) { + super(message == null ? "(" + command.getClass().getName() + " " + arguments.toString() + ")" + : message + " (" + command.getClass().getName() + " " + arguments.toString() + ")", e); + this.command = command; + this.arguments = arguments; + } + + public DescribedCommand getCommand() { + return command; + } + + public List getArguments() { + return arguments; + } + +} diff --git a/org.argeo.slc.runtime/src/org/argeo/cli/CommandsCli.java b/org.argeo.slc.runtime/src/org/argeo/cli/CommandsCli.java new file mode 100644 index 000000000..b0879f0a4 --- /dev/null +++ b/org.argeo.slc.runtime/src/org/argeo/cli/CommandsCli.java @@ -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 { + public final static String HELP = "help"; + + private final String commandName; + private Map, ?>> commands = new TreeMap<>(); + + protected final Options options = new Options(); + + public CommandsCli(String commandName) { + this.commandName = commandName; + } + + @Override + public Object apply(List args) { + String cmd = null; + List newArgs = new ArrayList<>(); + try { + CommandLineParser clParser = new DefaultParser(); + CommandLine commonCl = clParser.parse(getOptions(), args.toArray(new String[args.size()]), true); + List leftOvers = commonCl.getArgList(); + for (String arg : leftOvers) { + if (!arg.startsWith("-") && cmd == null) { + cmd = arg; + } else { + newArgs.add(arg); + } + } + } catch (ParseException e) { + CommandArgsException cae = new CommandArgsException(e); + throw cae; + } + + Function, ?> function = cmd != null ? getCommand(cmd) : getDefaultCommand(); + if (function == null) + throw new IllegalArgumentException("Uknown command " + cmd); + try { + return function.apply(newArgs).toString(); + } catch (CommandArgsException e) { + if (e.getCommandName() == null) { + e.setCommandName(cmd); + e.setCommandsCli(this); + } + throw e; + } catch (IllegalArgumentException e) { + CommandArgsException cae = new CommandArgsException(e); + cae.setCommandName(cmd); + throw cae; + } + } + + @Override + public Options getOptions() { + return options; + } + + protected void addCommand(String cmd, Function, ?> function) { + commands.put(cmd, function); + + } + + @Override + public String getUsage() { + return "[command]"; + } + + protected void addCommandsCli(CommandsCli commandsCli) { + addCommand(commandsCli.getCommandName(), commandsCli); + commandsCli.addCommand(HELP, new HelpCommand(this, commandsCli)); + } + + public String getCommandName() { + return commandName; + } + + public Set getSubCommands() { + return commands.keySet(); + } + + public Function, ?> getCommand(String command) { + return commands.get(command); + } + + public HelpCommand getHelpCommand() { + return (HelpCommand) getCommand(HELP); + } + + public Function, String> getDefaultCommand() { + return getHelpCommand(); + } + + /** In order to implement quickly a main method. */ + public static void mainImpl(CommandsCli cli, String[] args) { + try { + cli.addCommand(CommandsCli.HELP, new HelpCommand(null, cli)); + Object output = cli.apply(Arrays.asList(args)); + System.out.println(output); + System.exit(0); + } catch (CommandArgsException e) { + System.err.println("Wrong arguments " + Arrays.toString(args) + ": " + e.getMessage()); + if (e.getCommandName() != null) { + StringWriter out = new StringWriter(); + HelpCommand.printHelp(e.getCommandsCli(), e.getCommandName(), out); + System.err.println(out.toString()); + } else { + e.printStackTrace(); + } + System.exit(1); + } catch (Exception e) { + e.printStackTrace(); + System.exit(1); + } + } +} diff --git a/org.argeo.slc.runtime/src/org/argeo/cli/DescribedCommand.java b/org.argeo.slc.runtime/src/org/argeo/cli/DescribedCommand.java new file mode 100644 index 000000000..9587206b8 --- /dev/null +++ b/org.argeo.slc.runtime/src/org/argeo/cli/DescribedCommand.java @@ -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 extends Function, T> { + default Options getOptions() { + return new Options(); + } + + String getDescription(); + + default String getUsage() { + return null; + } + + default String getExamples() { + return null; + } + + default CommandLine toCommandLine(List args) { + try { + DefaultParser parser = new DefaultParser(); + return parser.parse(getOptions(), args.toArray(new String[args.size()])); + } catch (ParseException e) { + throw new CommandArgsException(e); + } + } + + /** In order to implement quickly a main method. */ + public static void mainImpl(DescribedCommand command, String[] args) { + try { + Object output = command.apply(Arrays.asList(args)); + System.out.println(output); + System.exit(0); + } catch (IllegalArgumentException e) { + StringWriter out = new StringWriter(); + HelpCommand.printHelp(command, out); + System.err.println(out.toString()); + System.exit(1); + } catch (Exception e) { + e.printStackTrace(); + System.exit(1); + } + } + +} diff --git a/org.argeo.slc.runtime/src/org/argeo/cli/HelpCommand.java b/org.argeo.slc.runtime/src/org/argeo/cli/HelpCommand.java new file mode 100644 index 000000000..755ce599e --- /dev/null +++ b/org.argeo.slc.runtime/src/org/argeo/cli/HelpCommand.java @@ -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 { + private CommandsCli commandsCli; + private CommandsCli parentCommandsCli; + + // Help formatting + private static int helpWidth = 80; + private static int helpLeftPad = 4; + private static int helpDescPad = 20; + + public HelpCommand(CommandsCli parentCommandsCli, CommandsCli commandsCli) { + super(); + this.parentCommandsCli = parentCommandsCli; + this.commandsCli = commandsCli; + } + + @Override + public String apply(List args) { + StringWriter out = new StringWriter(); + + if (args.size() == 0) {// overview + printHelp(commandsCli, out); + } else { + String cmd = args.get(0); + Function, ?> function = commandsCli.getCommand(cmd); + if (function == null) + return "Command " + cmd + " not found."; + Options options; + String examples; + DescribedCommand command = null; + if (function instanceof DescribedCommand) { + command = (DescribedCommand) function; + options = command.getOptions(); + examples = command.getExamples(); + } else { + options = new Options(); + examples = null; + } + String description = getShortDescription(function); + String commandCall = getCommandUsage(cmd, command); + HelpFormatter formatter = new HelpFormatter(); + formatter.printHelp(new PrintWriter(out), helpWidth, commandCall, description, options, helpLeftPad, + helpDescPad, examples, false); + } + return out.toString(); + } + + private static String getShortDescription(Function, ?> function) { + if (function instanceof DescribedCommand) { + return ((DescribedCommand) function).getDescription(); + } else { + return function.toString(); + } + } + + public String getCommandUsage(String cmd, DescribedCommand command) { + String commandCall = getCommandCall(commandsCli) + " " + cmd; + assert command != null; + if (command != null && command.getUsage() != null) { + commandCall = commandCall + " " + command.getUsage(); + } + return commandCall; + } + + @Override + public String getDescription() { + return "Shows this help or describes a command"; + } + + @Override + public String getUsage() { + return "[command]"; + } + + public CommandsCli getParentCommandsCli() { + return parentCommandsCli; + } + + protected String getCommandCall(CommandsCli commandsCli) { + HelpCommand hc = commandsCli.getHelpCommand(); + if (hc.getParentCommandsCli() != null) { + return getCommandCall(hc.getParentCommandsCli()) + " " + commandsCli.getCommandName(); + } else { + return commandsCli.getCommandName(); + } + } + + public static void printHelp(DescribedCommand command, StringWriter out) { + String usage = "java " + command.getClass().getName() + + (command.getUsage() != null ? " " + command.getUsage() : ""); + HelpFormatter formatter = new HelpFormatter(); + formatter.printHelp(new PrintWriter(out), helpWidth, usage, command.getDescription(), command.getOptions(), + helpLeftPad, helpDescPad, command.getExamples(), false); + + } + + public static void printHelp(CommandsCli commandsCli, String commandName, StringWriter out) { + DescribedCommand command = (DescribedCommand) commandsCli.getCommand(commandName); + String usage = commandsCli.getHelpCommand().getCommandUsage(commandName, command); + HelpFormatter formatter = new HelpFormatter(); + formatter.printHelp(new PrintWriter(out), helpWidth, usage, command.getDescription(), command.getOptions(), + helpLeftPad, helpDescPad, command.getExamples(), false); + + } + + public static void printHelp(CommandsCli commandsCli, StringWriter out) { + out.append(commandsCli.getDescription()).append('\n'); + String leftPad = spaces(helpLeftPad); + for (String cmd : commandsCli.getSubCommands()) { + Function, ?> function = commandsCli.getCommand(cmd); + assert function != null; + out.append(leftPad); + out.append(cmd); + // TODO deal with long commands + out.append(spaces(helpDescPad - cmd.length())); + out.append(getShortDescription(function)); + out.append('\n'); + } + } + + private static String spaces(int count) { + // Java 11 + // return " ".repeat(count); + if (count <= 0) + return ""; + else { + StringBuilder sb = new StringBuilder(count); + for (int i = 0; i < count; i++) + sb.append(' '); + return sb.toString(); + } + } +} diff --git a/org.argeo.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 index 000000000..ba529eae2 --- /dev/null +++ b/org.argeo.slc.runtime/src/org/argeo/cli/fs/FileSync.java @@ -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> { + final static Option deleteOption = Option.builder().longOpt("delete").desc("delete from target").build(); + final static Option recursiveOption = Option.builder("r").longOpt("recursive").desc("recurse into directories") + .build(); + final static Option progressOption = Option.builder().longOpt("progress").hasArg(false).desc("show progress") + .build(); + + @Override + public SyncResult apply(List t) { + try { + CommandLine line = toCommandLine(t); + List remaining = line.getArgList(); + if (remaining.size() == 0) { + throw new CommandArgsException("There must be at least one argument"); + } + URI sourceUri = new URI(remaining.get(0)); + URI targetUri; + if (remaining.size() == 1) { + targetUri = Paths.get(System.getProperty("user.dir")).toUri(); + } else { + targetUri = new URI(remaining.get(1)); + } + boolean delete = line.hasOption(deleteOption.getLongOpt()); + boolean recursive = line.hasOption(recursiveOption.getLongOpt()); + PathSync pathSync = new PathSync(sourceUri, targetUri, delete, recursive); + return pathSync.call(); + } catch (URISyntaxException e) { + throw new CommandArgsException(e); + } + } + + @Override + public Options getOptions() { + Options options = new Options(); + options.addOption(recursiveOption); + options.addOption(deleteOption); + options.addOption(progressOption); + return options; + } + + @Override + public String getUsage() { + return "[source URI] [target URI]"; + } + + public static void main(String[] args) { + DescribedCommand.mainImpl(new FileSync(), args); +// Options options = new Options(); +// options.addOption("r", "recursive", false, "recurse into directories"); +// options.addOption(Option.builder().longOpt("progress").hasArg(false).desc("show progress").build()); +// +// CommandLineParser parser = new DefaultParser(); +// try { +// CommandLine line = parser.parse(options, args); +// List remaining = line.getArgList(); +// if (remaining.size() == 0) { +// System.err.println("There must be at least one argument"); +// printHelp(options); +// System.exit(1); +// } +// URI sourceUri = new URI(remaining.get(0)); +// URI targetUri; +// if (remaining.size() == 1) { +// targetUri = Paths.get(System.getProperty("user.dir")).toUri(); +// } else { +// targetUri = new URI(remaining.get(1)); +// } +// PathSync pathSync = new PathSync(sourceUri, targetUri); +// pathSync.run(); +// } catch (Exception exp) { +// exp.printStackTrace(); +// printHelp(options); +// System.exit(1); +// } + } + +// public static void printHelp(Options options) { +// HelpFormatter formatter = new HelpFormatter(); +// formatter.printHelp("sync SRC [DEST]", options, true); +// } + + @Override + public String getDescription() { + return "Synchronises files"; + } + +} diff --git a/org.argeo.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 index 000000000..c08ad0091 --- /dev/null +++ b/org.argeo.slc.runtime/src/org/argeo/cli/fs/FsCommands.java @@ -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 index 000000000..9ab9cafad --- /dev/null +++ b/org.argeo.slc.runtime/src/org/argeo/cli/fs/PathSync.java @@ -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> { + private final URI sourceUri, targetUri; + private final boolean delete; + private final boolean recursive; + + public PathSync(URI sourceUri, URI targetUri) { + this(sourceUri, targetUri, false, false); + } + + public PathSync(URI sourceUri, URI targetUri, boolean delete, boolean recursive) { + this.sourceUri = sourceUri; + this.targetUri = targetUri; + this.delete = delete; + this.recursive = recursive; + } + + @Override + public SyncResult call() { + try { + Path sourceBasePath = createPath(sourceUri); + Path targetBasePath = createPath(targetUri); + SyncFileVisitor syncFileVisitor = new SyncFileVisitor(sourceBasePath, targetBasePath, delete, recursive); + Files.walkFileTree(sourceBasePath, syncFileVisitor); + return syncFileVisitor.getSyncResult(); + } catch (Exception e) { + throw new IllegalStateException("Cannot sync " + sourceUri + " to " + targetUri, e); + } + } + + private Path createPath(URI uri) { + Path path; + if (uri.getScheme() == null) { + path = Paths.get(uri.getPath()); + } else if (uri.getScheme().equals("file")) { + FileSystemProvider fsProvider = FileSystems.getDefault().provider(); + path = fsProvider.getPath(uri); + } else if (uri.getScheme().equals("davex")) { + throw new UnsupportedOperationException(); +// FileSystemProvider fsProvider = new DavexFsProvider(); +// path = fsProvider.getPath(uri); +// } else if (uri.getScheme().equals("sftp")) { +// Sftp sftp = new Sftp(uri); +// path = sftp.getBasePath(); + } else + throw new IllegalArgumentException("URI scheme not supported for " + uri); + return path; + } +} diff --git a/org.argeo.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 index 000000000..892df5060 --- /dev/null +++ b/org.argeo.slc.runtime/src/org/argeo/cli/fs/SyncFileVisitor.java @@ -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 index 000000000..8ad42b25d --- /dev/null +++ b/org.argeo.slc.runtime/src/org/argeo/cli/fs/package-info.java @@ -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 index 000000000..23895935f --- /dev/null +++ b/org.argeo.slc.runtime/src/org/argeo/cli/package-info.java @@ -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 index 000000000..5746ebd0f --- /dev/null +++ b/org.argeo.slc.runtime/src/org/argeo/cli/posix/Echo.java @@ -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 { + + @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 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 index 000000000..bb6af67b8 --- /dev/null +++ b/org.argeo.slc.runtime/src/org/argeo/cli/posix/PosixCommands.java @@ -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 index 000000000..b0d1a46f9 --- /dev/null +++ b/org.argeo.slc.runtime/src/org/argeo/cli/posix/package-info.java @@ -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 index 000000000..03bac592c --- /dev/null +++ b/org.argeo.slc.runtime/src/org/argeo/fs/BasicSyncFileVisitor.java @@ -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 { + // TODO make it configurable + private boolean trace = false; + + private final Path sourceBasePath; + private final Path targetBasePath; + private final boolean delete; + private final boolean recursive; + + private SyncResult syncResult = new SyncResult<>(); + + public BasicSyncFileVisitor(Path sourceBasePath, Path targetBasePath, boolean delete, boolean recursive) { + this.sourceBasePath = sourceBasePath; + this.targetBasePath = targetBasePath; + this.delete = delete; + this.recursive = recursive; + } + + @Override + public FileVisitResult preVisitDirectory(Path sourceDir, BasicFileAttributes attrs) throws IOException { + if (!recursive && !sourceDir.equals(sourceBasePath)) + return FileVisitResult.SKIP_SUBTREE; + Path targetDir = toTargetPath(sourceDir); + Files.createDirectories(targetDir); + return FileVisitResult.CONTINUE; + } + + @Override + public FileVisitResult postVisitDirectory(Path sourceDir, IOException exc) throws IOException { + if (delete) { + Path targetDir = toTargetPath(sourceDir); + for (Path targetPath : Files.newDirectoryStream(targetDir)) { + Path sourcePath = sourceDir.resolve(targetPath.getFileName()); + if (!Files.exists(sourcePath)) { + try { + 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 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 index 000000000..c96f56ed2 --- /dev/null +++ b/org.argeo.slc.runtime/src/org/argeo/fs/FsUtils.java @@ -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() { + @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 index 000000000..ea2de9ed6 --- /dev/null +++ b/org.argeo.slc.runtime/src/org/argeo/fs/package-info.java @@ -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 index 000000000..21cca7547 --- /dev/null +++ b/org.argeo.slc.runtime/src/org/argeo/slc/runtime/ArgeoCli.java @@ -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 index 000000000..89bf869a2 --- /dev/null +++ b/org.argeo.slc.runtime/src/org/argeo/sync/SyncException.java @@ -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 index 000000000..6d12ada4a --- /dev/null +++ b/org.argeo.slc.runtime/src/org/argeo/sync/SyncResult.java @@ -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 { + private final Set added = new TreeSet<>(); + private final Set modified = new TreeSet<>(); + private final Set deleted = new TreeSet<>(); + private final Set errors = new TreeSet<>(); + + public Set getAdded() { + return added; + } + + public Set getModified() { + return modified; + } + + public Set getDeleted() { + return deleted; + } + + public Set getErrors() { + return errors; + } + + public void addError(T sourcePath, T targetPath, Exception e) { + Error error = new Error(sourcePath, targetPath, e); + errors.add(error); + } + + public boolean noModification() { + return modified.isEmpty() && deleted.isEmpty() && added.isEmpty(); + } + + @Override + public String toString() { + if (noModification()) + return "No modification."; + StringBuffer sb = new StringBuffer(); + for (T p : modified) + sb.append("MOD ").append(p).append('\n'); + for (T p : deleted) + sb.append("DEL ").append(p).append('\n'); + for (T p : added) + sb.append("ADD ").append(p).append('\n'); + for (Error error : errors) + sb.append(error).append('\n'); + return sb.toString(); + } + + public class Error implements Comparable { + private final T sourcePath;// if null this is a failed delete + private final T targetPath; + private final Exception exception; + private final Instant timestamp = Instant.now(); + + public Error(T sourcePath, T targetPath, Exception e) { + super(); + this.sourcePath = sourcePath; + this.targetPath = targetPath; + this.exception = e; + } + + public T getSourcePath() { + return sourcePath; + } + + public T getTargetPath() { + return targetPath; + } + + public Exception getException() { + return exception; + } + + public Instant getTimestamp() { + return timestamp; + } + + @Override + public int compareTo(Error o) { + return timestamp.compareTo(o.timestamp); + } + + @Override + public int hashCode() { + return timestamp.hashCode(); + } + + @Override + public String toString() { + return "ERR " + timestamp + (sourcePath == null ? "Deletion failed" : "Copy failed " + sourcePath) + " " + + targetPath + " " + exception.getMessage(); + } + + } +} diff --git a/org.argeo.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 index 000000000..c5e9da0f6 --- /dev/null +++ b/org.argeo.slc.runtime/src/org/argeo/sync/package-info.java @@ -0,0 +1,2 @@ +/** Synchrnoisation related utilities. */ +package org.argeo.sync; \ No newline at end of file -- 2.39.2