<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>
</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>
<!-- 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>
<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>
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;
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;
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;
--- /dev/null
+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());
+ }
+
+}
--- /dev/null
+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";
+ }
+}
--- /dev/null
+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";
+}
--- /dev/null
+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);
+ }
+
+}
<!-- 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>
<!-- 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>
</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>
-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
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
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
--- /dev/null
+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";
+ }
+
+}
--- /dev/null
+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";
+ }
+
+}
--- /dev/null
+/** JCR CLI commands. */
+package org.argeo.cli.jcr;
\ No newline at end of file
--- /dev/null
+<?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
<?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>
--- /dev/null
+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;
+ }
+}
--- /dev/null
+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;
+ }
+
+}
--- /dev/null
+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;
+ }
+
+}
--- /dev/null
+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);
+ }
+ }
+}
--- /dev/null
+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);
+ }
+ }
+
+}
--- /dev/null
+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();
+ }
+ }
+}
--- /dev/null
+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";
+ }
+
+}
--- /dev/null
+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";
+ }
+
+}
--- /dev/null
+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;
+ }
+}
--- /dev/null
+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);
+ }
+}
--- /dev/null
+/** File system CLI commands. */
+package org.argeo.cli.fs;
\ No newline at end of file
--- /dev/null
+/** Command line API. */
+package org.argeo.cli;
\ No newline at end of file
--- /dev/null
+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();
+ }
+
+}
--- /dev/null
+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);
+ }
+}
--- /dev/null
+/** Posix CLI commands. */
+package org.argeo.cli.posix;
\ No newline at end of file
--- /dev/null
+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;
+ }
+
+}
--- /dev/null
+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() {
+ }
+
+}
--- /dev/null
+/** Generic file system utilities. */
+package org.argeo.fs;
\ No newline at end of file
--- /dev/null
+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);
+ }
+
+}
--- /dev/null
+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);
+ }
+}
--- /dev/null
+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();
+ }
+
+ }
+}
--- /dev/null
+/** Synchrnoisation related utilities. */
+package org.argeo.sync;
\ No newline at end of file