From: Mathieu Baudier Date: Sun, 5 Dec 2021 05:16:18 +0000 (+0100) Subject: Put all JCR projects together. X-Git-Tag: argeo-commons-2.3.5~135 X-Git-Url: https://git.argeo.org/?a=commitdiff_plain;h=623a0db2d0f161c101b9e41abcaccc04d478d32a;p=lgpl%2Fargeo-commons.git Put all JCR projects together. --- diff --git a/dep/org.argeo.dep.cms.client/pom.xml b/dep/org.argeo.dep.cms.client/pom.xml index 423b9b32c..2381d27b3 100644 --- a/dep/org.argeo.dep.cms.client/pom.xml +++ b/dep/org.argeo.dep.cms.client/pom.xml @@ -17,11 +17,11 @@ org.argeo.enterprise 2.3-SNAPSHOT - - org.argeo.commons - org.argeo.jcr - 2.3-SNAPSHOT - + + + + + org.argeo.commons org.argeo.core diff --git a/dep/org.argeo.dep.cms.node/pom.xml b/dep/org.argeo.dep.cms.node/pom.xml index f88a1ded9..4afc1f41a 100644 --- a/dep/org.argeo.dep.cms.node/pom.xml +++ b/dep/org.argeo.dep.cms.node/pom.xml @@ -9,6 +9,7 @@ org.argeo.dep.cms.node CMS Node + @@ -26,15 +27,21 @@ 2.3-SNAPSHOT + org.argeo.commons org.argeo.cms 2.3-SNAPSHOT org.argeo.commons - org.argeo.maintenance + org.argeo.cms.jcr 2.3-SNAPSHOT + + + + + diff --git a/org.argeo.cms.jcr/.classpath b/org.argeo.cms.jcr/.classpath new file mode 100644 index 000000000..20cad808a --- /dev/null +++ b/org.argeo.cms.jcr/.classpath @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/org.argeo.cms.jcr/.project b/org.argeo.cms.jcr/.project new file mode 100644 index 000000000..9b33a445c --- /dev/null +++ b/org.argeo.cms.jcr/.project @@ -0,0 +1,28 @@ + + + org.argeo.cms.jcr + + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.eclipse.pde.ManifestBuilder + + + + + org.eclipse.pde.SchemaBuilder + + + + + + org.eclipse.pde.PluginNature + org.eclipse.jdt.core.javanature + + diff --git a/org.argeo.cms.jcr/.settings/org.eclipse.jdt.core.prefs b/org.argeo.cms.jcr/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 000000000..7e2e11935 --- /dev/null +++ b/org.argeo.cms.jcr/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,101 @@ +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.annotation.inheritNullAnnotations=disabled +org.eclipse.jdt.core.compiler.annotation.missingNonNullByDefaultAnnotation=ignore +org.eclipse.jdt.core.compiler.annotation.nonnull=org.eclipse.jdt.annotation.NonNull +org.eclipse.jdt.core.compiler.annotation.nonnull.secondary= +org.eclipse.jdt.core.compiler.annotation.nonnullbydefault=org.eclipse.jdt.annotation.NonNullByDefault +org.eclipse.jdt.core.compiler.annotation.nonnullbydefault.secondary= +org.eclipse.jdt.core.compiler.annotation.nullable=org.eclipse.jdt.annotation.Nullable +org.eclipse.jdt.core.compiler.annotation.nullable.secondary= +org.eclipse.jdt.core.compiler.annotation.nullanalysis=disabled +org.eclipse.jdt.core.compiler.problem.APILeak=warning +org.eclipse.jdt.core.compiler.problem.annotationSuperInterface=warning +org.eclipse.jdt.core.compiler.problem.autoboxing=ignore +org.eclipse.jdt.core.compiler.problem.comparingIdentical=warning +org.eclipse.jdt.core.compiler.problem.deadCode=warning +org.eclipse.jdt.core.compiler.problem.deprecation=warning +org.eclipse.jdt.core.compiler.problem.deprecationInDeprecatedCode=disabled +org.eclipse.jdt.core.compiler.problem.deprecationWhenOverridingDeprecatedMethod=disabled +org.eclipse.jdt.core.compiler.problem.discouragedReference=warning +org.eclipse.jdt.core.compiler.problem.emptyStatement=ignore +org.eclipse.jdt.core.compiler.problem.explicitlyClosedAutoCloseable=ignore +org.eclipse.jdt.core.compiler.problem.fallthroughCase=ignore +org.eclipse.jdt.core.compiler.problem.fatalOptionalError=disabled +org.eclipse.jdt.core.compiler.problem.fieldHiding=ignore +org.eclipse.jdt.core.compiler.problem.finalParameterBound=warning +org.eclipse.jdt.core.compiler.problem.finallyBlockNotCompletingNormally=warning +org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning +org.eclipse.jdt.core.compiler.problem.hiddenCatchBlock=warning +org.eclipse.jdt.core.compiler.problem.includeNullInfoFromAsserts=disabled +org.eclipse.jdt.core.compiler.problem.incompatibleNonInheritedInterfaceMethod=warning +org.eclipse.jdt.core.compiler.problem.incompleteEnumSwitch=warning +org.eclipse.jdt.core.compiler.problem.indirectStaticAccess=ignore +org.eclipse.jdt.core.compiler.problem.localVariableHiding=ignore +org.eclipse.jdt.core.compiler.problem.methodWithConstructorName=warning +org.eclipse.jdt.core.compiler.problem.missingDefaultCase=ignore +org.eclipse.jdt.core.compiler.problem.missingDeprecatedAnnotation=ignore +org.eclipse.jdt.core.compiler.problem.missingEnumCaseDespiteDefault=disabled +org.eclipse.jdt.core.compiler.problem.missingHashCodeMethod=ignore +org.eclipse.jdt.core.compiler.problem.missingOverrideAnnotation=ignore +org.eclipse.jdt.core.compiler.problem.missingOverrideAnnotationForInterfaceMethodImplementation=enabled +org.eclipse.jdt.core.compiler.problem.missingSerialVersion=warning +org.eclipse.jdt.core.compiler.problem.missingSynchronizedOnInheritedMethod=ignore +org.eclipse.jdt.core.compiler.problem.noEffectAssignment=warning +org.eclipse.jdt.core.compiler.problem.noImplicitStringConversion=warning +org.eclipse.jdt.core.compiler.problem.nonExternalizedStringLiteral=ignore +org.eclipse.jdt.core.compiler.problem.nonnullParameterAnnotationDropped=warning +org.eclipse.jdt.core.compiler.problem.nonnullTypeVariableFromLegacyInvocation=warning +org.eclipse.jdt.core.compiler.problem.nullAnnotationInferenceConflict=error +org.eclipse.jdt.core.compiler.problem.nullReference=warning +org.eclipse.jdt.core.compiler.problem.nullSpecViolation=error +org.eclipse.jdt.core.compiler.problem.nullUncheckedConversion=warning +org.eclipse.jdt.core.compiler.problem.overridingPackageDefaultMethod=warning +org.eclipse.jdt.core.compiler.problem.parameterAssignment=ignore +org.eclipse.jdt.core.compiler.problem.pessimisticNullAnalysisForFreeTypeVariables=warning +org.eclipse.jdt.core.compiler.problem.possibleAccidentalBooleanAssignment=ignore +org.eclipse.jdt.core.compiler.problem.potentialNullReference=ignore +org.eclipse.jdt.core.compiler.problem.potentiallyUnclosedCloseable=ignore +org.eclipse.jdt.core.compiler.problem.rawTypeReference=warning +org.eclipse.jdt.core.compiler.problem.redundantNullAnnotation=warning +org.eclipse.jdt.core.compiler.problem.redundantNullCheck=ignore +org.eclipse.jdt.core.compiler.problem.redundantSpecificationOfTypeArguments=ignore +org.eclipse.jdt.core.compiler.problem.redundantSuperinterface=ignore +org.eclipse.jdt.core.compiler.problem.reportMethodCanBePotentiallyStatic=ignore +org.eclipse.jdt.core.compiler.problem.reportMethodCanBeStatic=ignore +org.eclipse.jdt.core.compiler.problem.specialParameterHidingField=disabled +org.eclipse.jdt.core.compiler.problem.staticAccessReceiver=warning +org.eclipse.jdt.core.compiler.problem.suppressOptionalErrors=disabled +org.eclipse.jdt.core.compiler.problem.suppressWarnings=enabled +org.eclipse.jdt.core.compiler.problem.syntacticNullAnalysisForFields=disabled +org.eclipse.jdt.core.compiler.problem.syntheticAccessEmulation=ignore +org.eclipse.jdt.core.compiler.problem.terminalDeprecation=warning +org.eclipse.jdt.core.compiler.problem.typeParameterHiding=warning +org.eclipse.jdt.core.compiler.problem.unavoidableGenericTypeProblems=enabled +org.eclipse.jdt.core.compiler.problem.uncheckedTypeOperation=warning +org.eclipse.jdt.core.compiler.problem.unclosedCloseable=warning +org.eclipse.jdt.core.compiler.problem.undocumentedEmptyBlock=ignore +org.eclipse.jdt.core.compiler.problem.unhandledWarningToken=warning +org.eclipse.jdt.core.compiler.problem.unlikelyCollectionMethodArgumentType=warning +org.eclipse.jdt.core.compiler.problem.unlikelyCollectionMethodArgumentTypeStrict=disabled +org.eclipse.jdt.core.compiler.problem.unlikelyEqualsArgumentType=info +org.eclipse.jdt.core.compiler.problem.unnecessaryElse=ignore +org.eclipse.jdt.core.compiler.problem.unnecessaryTypeCheck=ignore +org.eclipse.jdt.core.compiler.problem.unqualifiedFieldAccess=ignore +org.eclipse.jdt.core.compiler.problem.unstableAutoModuleName=warning +org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownException=ignore +org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionExemptExceptionAndThrowable=enabled +org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionIncludeDocCommentReference=enabled +org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionWhenOverriding=disabled +org.eclipse.jdt.core.compiler.problem.unusedExceptionParameter=ignore +org.eclipse.jdt.core.compiler.problem.unusedImport=warning +org.eclipse.jdt.core.compiler.problem.unusedLabel=warning +org.eclipse.jdt.core.compiler.problem.unusedLocal=warning +org.eclipse.jdt.core.compiler.problem.unusedObjectAllocation=ignore +org.eclipse.jdt.core.compiler.problem.unusedParameter=ignore +org.eclipse.jdt.core.compiler.problem.unusedParameterIncludeDocCommentReference=enabled +org.eclipse.jdt.core.compiler.problem.unusedParameterWhenImplementingAbstract=disabled +org.eclipse.jdt.core.compiler.problem.unusedParameterWhenOverridingConcrete=disabled +org.eclipse.jdt.core.compiler.problem.unusedPrivateMember=warning +org.eclipse.jdt.core.compiler.problem.unusedTypeParameter=ignore +org.eclipse.jdt.core.compiler.problem.unusedWarningToken=warning +org.eclipse.jdt.core.compiler.problem.varargsArgumentNeedCast=warning diff --git a/org.argeo.cms.jcr/bnd.bnd b/org.argeo.cms.jcr/bnd.bnd new file mode 100644 index 000000000..5a8e8ab0b --- /dev/null +++ b/org.argeo.cms.jcr/bnd.bnd @@ -0,0 +1,12 @@ +Provide-Capability:\ +cms.datamodel; name=jcrx; cnd=/org/argeo/jcr/jcrx.cnd; abstract=true + +Import-Package:\ +org.apache.jackrabbit.api,\ +org.apache.jackrabbit.commons,\ +org.apache.jackrabbit.spi,\ +org.apache.jackrabbit.spi2dav,\ +org.apache.jackrabbit.spi2davex,\ +org.apache.jackrabbit.webdav,\ +junit.*;resolution:=optional,\ +* \ No newline at end of file diff --git a/org.argeo.cms.jcr/build.properties b/org.argeo.cms.jcr/build.properties new file mode 100644 index 000000000..8667a0edf --- /dev/null +++ b/org.argeo.cms.jcr/build.properties @@ -0,0 +1,30 @@ +source.. = src/,\ + ext/test/ +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 + \ No newline at end of file diff --git a/org.argeo.cms.jcr/ext/test/org/argeo/jcr/fs/JcrFileSystemTest.java b/org.argeo.cms.jcr/ext/test/org/argeo/jcr/fs/JcrFileSystemTest.java new file mode 100644 index 000000000..2d03b8f2c --- /dev/null +++ b/org.argeo.cms.jcr/ext/test/org/argeo/jcr/fs/JcrFileSystemTest.java @@ -0,0 +1,191 @@ +package org.argeo.jcr.fs; + +import java.io.InputStream; +import java.io.OutputStream; +import java.net.URI; +import java.nio.file.DirectoryStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.attribute.BasicFileAttributes; +import java.nio.file.attribute.FileTime; +import java.nio.file.spi.FileSystemProvider; +import java.util.Arrays; +import java.util.Map; + +import javax.jcr.Property; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.nodetype.NodeType; + +import org.apache.commons.io.IOUtils; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.jackrabbit.core.RepositoryImpl; +import org.argeo.jackrabbit.fs.JackrabbitMemoryFsProvider; + +import junit.framework.TestCase; + +public class JcrFileSystemTest extends TestCase { + private final static Log log = LogFactory.getLog(JcrFileSystemTest.class); + + public void testMounts() throws Exception { + JackrabbitMemoryFsProvider fsProvider = new JackrabbitMemoryFsProvider() { + + @Override + protected void postRepositoryCreation(RepositoryImpl repositoryImpl) throws RepositoryException { + // create workspace + Session session = login(); + session.getWorkspace().createWorkspace("test"); + } + + }; + + Path rootPath = fsProvider.getPath(new URI("jcr+memory:/")); + log.debug("Got root " + rootPath); + Path testDir = rootPath.resolve("testDir"); + Files.createDirectory(testDir); + + Path testMount = fsProvider.getPath(new URI("jcr+memory:/test")); + log.debug("Test path"); + assertEquals(rootPath, testMount.getParent()); + assertEquals(testMount.getFileName(), rootPath.relativize(testMount)); + + Path testPath = testMount.resolve("test.txt"); + log.debug("Create file " + testPath); + Files.createFile(testPath); + BasicFileAttributes bfa = Files.readAttributes(testPath, BasicFileAttributes.class); + FileTime ft = bfa.creationTime(); + assertNotNull(ft); + assertTrue(bfa.isRegularFile()); + log.debug("Created " + testPath + " (" + ft + ")"); + Files.delete(testPath); + log.debug("Deleted " + testPath); + + // Browse directories from root + DirectoryStream files = Files.newDirectoryStream(rootPath); + int directoryCount = 0; + for (Path file : files) { + if (Files.isDirectory(file)) { + directoryCount++; + } + } + assertEquals(2, directoryCount); + + // Browse directories from mount + Path mountSubDir = testMount.resolve("mountSubDir"); + Files.createDirectory(mountSubDir); + Path otherSubDir = testMount.resolve("otherSubDir"); + Files.createDirectory(otherSubDir); + testPath = testMount.resolve("test.txt"); + Files.createFile(testPath); + files = Files.newDirectoryStream(testMount); + int fileCount = 0; + for (Path file : files) { + fileCount++; + } + assertEquals(3, fileCount); + + } + + public void testSimple() throws Exception { + FileSystemProvider fsProvider = new JackrabbitMemoryFsProvider(); + + // Simple file + Path rootPath = fsProvider.getPath(new URI("jcr+memory:/")); + log.debug("Got root " + rootPath); + Path testPath = fsProvider.getPath(new URI("jcr+memory:/test.txt")); + log.debug("Test path"); + assertEquals("test.txt", testPath.getFileName().toString()); + assertEquals(rootPath, testPath.getParent()); + assertEquals(testPath.getFileName(), rootPath.relativize(testPath)); + // relativize self should be empty path + Path selfRelative = testPath.relativize(testPath); + assertEquals("", selfRelative.toString()); + + log.debug("Create file " + testPath); + Files.createFile(testPath); + BasicFileAttributes bfa = Files.readAttributes(testPath, BasicFileAttributes.class); + FileTime ft = bfa.creationTime(); + assertNotNull(ft); + assertTrue(bfa.isRegularFile()); + log.debug("Created " + testPath + " (" + ft + ")"); + Files.delete(testPath); + log.debug("Deleted " + testPath); + String txt = "TEST\nTEST2\n"; + byte[] arr = txt.getBytes(); + Files.write(testPath, arr); + log.debug("Wrote " + testPath); + byte[] read = Files.readAllBytes(testPath); + assertTrue(Arrays.equals(arr, read)); + assertEquals(txt, new String(read)); + log.debug("Read " + testPath); + Path testDir = rootPath.resolve("testDir"); + log.debug("Resolved " + testDir); + // Copy + Files.createDirectory(testDir); + log.debug("Created directory " + testDir); + Path subsubdir = Files.createDirectories(testDir.resolve("subdir/subsubdir")); + log.debug("Created sub directories " + subsubdir); + Path copiedFile = testDir.resolve("copiedFile.txt"); + log.debug("Resolved " + copiedFile); + Path relativeCopiedFile = testDir.relativize(copiedFile); + assertEquals(copiedFile.getFileName().toString(), relativeCopiedFile.toString()); + log.debug("Relative copied file " + relativeCopiedFile); + try (OutputStream out = Files.newOutputStream(copiedFile); InputStream in = Files.newInputStream(testPath)) { + IOUtils.copy(in, out); + } + log.debug("Copied " + testPath + " to " + copiedFile); + Files.delete(testPath); + log.debug("Deleted " + testPath); + byte[] copiedRead = Files.readAllBytes(copiedFile); + assertTrue(Arrays.equals(copiedRead, read)); + log.debug("Read " + copiedFile); + // Browse directories + DirectoryStream files = Files.newDirectoryStream(testDir); + int fileCount = 0; + Path listedFile = null; + for (Path file : files) { + fileCount++; + if (!Files.isDirectory(file)) + listedFile = file; + } + assertEquals(2, fileCount); + assertEquals(copiedFile, listedFile); + assertEquals(copiedFile.toString(), listedFile.toString()); + log.debug("Listed " + testDir); + // Generic attributes + Map attrs = Files.readAttributes(copiedFile, "*"); + assertEquals(3, attrs.size()); + log.debug("Read attributes of " + copiedFile + ": " + attrs.keySet()); + // Direct node access + NodeFileAttributes nfa = Files.readAttributes(copiedFile, NodeFileAttributes.class); + nfa.getNode().addMixin(NodeType.MIX_LANGUAGE); + nfa.getNode().getSession().save(); + log.debug("Add mix:language"); + Files.setAttribute(copiedFile, Property.JCR_LANGUAGE, "fr"); + log.debug("Set language"); + attrs = Files.readAttributes(copiedFile, "*"); + assertEquals(4, attrs.size()); + log.debug("Read attributes of " + copiedFile + ": " + attrs.keySet()); + } + + public void testIllegalCharacters() throws Exception { + FileSystemProvider fsProvider = new JackrabbitMemoryFsProvider(); + String fileName = "tüßçt[1].txt"; + String pathStr = "/testDir/" + fileName; + Path testDir = fsProvider.getPath(new URI("jcr+memory:/testDir")); + Files.createDirectory(testDir); + Path testPath = testDir.resolve(fileName); + assertEquals(pathStr, testPath.toString()); + Files.createFile(testPath); + DirectoryStream files = Files.newDirectoryStream(testDir); + Path listedPath = files.iterator().next(); + assertEquals(pathStr, listedPath.toString()); + + String dirName = "*[~WeirdDir~]*"; + Path subDir = testDir.resolve(dirName); + Files.createDirectory(subDir); + subDir = testDir.resolve(dirName); + assertEquals(dirName, subDir.getFileName().toString()); + } +} diff --git a/org.argeo.cms.jcr/pom.xml b/org.argeo.cms.jcr/pom.xml new file mode 100644 index 000000000..396cabecb --- /dev/null +++ b/org.argeo.cms.jcr/pom.xml @@ -0,0 +1,29 @@ + + + 4.0.0 + + org.argeo.commons + argeo-commons + 2.3-SNAPSHOT + .. + + org.argeo.cms.jcr + Commons CMS JCR + + + org.argeo.commons + org.argeo.api + 2.3-SNAPSHOT + + + org.argeo.commons + org.argeo.enterprise + 2.3-SNAPSHOT + + + org.argeo.commons + org.argeo.core + 2.3-SNAPSHOT + + + \ No newline at end of file diff --git a/org.argeo.cms.jcr/src/org/argeo/cli/jcr/JcrCommands.java b/org.argeo.cms.jcr/src/org/argeo/cli/jcr/JcrCommands.java new file mode 100644 index 000000000..ea7467462 --- /dev/null +++ b/org.argeo.cms.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.cms.jcr/src/org/argeo/cli/jcr/JcrSync.java b/org.argeo.cms.jcr/src/org/argeo/cli/jcr/JcrSync.java new file mode 100644 index 000000000..401f447c9 --- /dev/null +++ b/org.argeo.cms.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.cms.jcr/src/org/argeo/cli/jcr/package-info.java b/org.argeo.cms.jcr/src/org/argeo/cli/jcr/package-info.java new file mode 100644 index 000000000..6f3f01f3a --- /dev/null +++ b/org.argeo.cms.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.cms.jcr/src/org/argeo/cli/jcr/repository-localfs.xml b/org.argeo.cms.jcr/src/org/argeo/cli/jcr/repository-localfs.xml new file mode 100644 index 000000000..5e7759cf4 --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/cli/jcr/repository-localfs.xml @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/org.argeo.cms.jcr/src/org/argeo/jackrabbit/JackrabbitAdminLoginModule.java b/org.argeo.cms.jcr/src/org/argeo/jackrabbit/JackrabbitAdminLoginModule.java new file mode 100644 index 000000000..7396c87e7 --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/jackrabbit/JackrabbitAdminLoginModule.java @@ -0,0 +1,48 @@ +package org.argeo.jackrabbit; + +import java.util.Map; + +import javax.security.auth.Subject; +import javax.security.auth.callback.CallbackHandler; +import javax.security.auth.login.LoginException; +import javax.security.auth.spi.LoginModule; + +import org.apache.jackrabbit.core.security.SecurityConstants; +import org.apache.jackrabbit.core.security.principal.AdminPrincipal; + +@Deprecated +public class JackrabbitAdminLoginModule implements LoginModule { + private Subject subject; + + @Override + public void initialize(Subject subject, CallbackHandler callbackHandler, + Map sharedState, Map options) { + this.subject = subject; + } + + @Override + public boolean login() throws LoginException { + // TODO check permission? + return true; + } + + @Override + public boolean commit() throws LoginException { + subject.getPrincipals().add( + new AdminPrincipal(SecurityConstants.ADMIN_ID)); + return true; + } + + @Override + public boolean abort() throws LoginException { + return true; + } + + @Override + public boolean logout() throws LoginException { + subject.getPrincipals().removeAll( + subject.getPrincipals(AdminPrincipal.class)); + return true; + } + +} diff --git a/org.argeo.cms.jcr/src/org/argeo/jackrabbit/JackrabbitDataModelMigration.java b/org.argeo.cms.jcr/src/org/argeo/jackrabbit/JackrabbitDataModelMigration.java new file mode 100644 index 000000000..9a49a063f --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/jackrabbit/JackrabbitDataModelMigration.java @@ -0,0 +1,174 @@ +package org.argeo.jackrabbit; + +import java.awt.geom.CubicCurve2D; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.Reader; +import java.net.URL; + +import javax.jcr.RepositoryException; +import javax.jcr.Session; + +import org.apache.commons.io.IOUtils; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.jackrabbit.commons.cnd.CndImporter; +import org.apache.jackrabbit.commons.cnd.ParseException; +import org.apache.jackrabbit.core.config.RepositoryConfig; +import org.apache.jackrabbit.core.fs.FileSystemException; +import org.argeo.jcr.JcrCallback; +import org.argeo.jcr.JcrException; +import org.argeo.jcr.JcrUtils; + +/** Migrate the data in a Jackrabbit repository. */ +@Deprecated +public class JackrabbitDataModelMigration implements Comparable { + private final static Log log = LogFactory.getLog(JackrabbitDataModelMigration.class); + + private String dataModelNodePath; + private String targetVersion; + private URL migrationCnd; + private JcrCallback dataModification; + + /** + * Expects an already started repository with the old data model to migrate. + * Expects to be run with admin rights (Repository.login() will be used). + * + * @return true if a migration was performed and the repository needs to be + * restarted and its caches cleared. + */ + public Boolean migrate(Session session) { + long begin = System.currentTimeMillis(); + Reader reader = null; + try { + // check if already migrated + if (!session.itemExists(dataModelNodePath)) { +// log.warn("Node " + dataModelNodePath + " does not exist: nothing to migrate."); + return false; + } +// Node dataModelNode = session.getNode(dataModelNodePath); +// if (dataModelNode.hasProperty(ArgeoNames.ARGEO_DATA_MODEL_VERSION)) { +// String currentVersion = dataModelNode.getProperty( +// ArgeoNames.ARGEO_DATA_MODEL_VERSION).getString(); +// if (compareVersions(currentVersion, targetVersion) >= 0) { +// log.info("Data model at version " + currentVersion +// + ", no need to migrate."); +// return false; +// } +// } + + // apply transitional CND + if (migrationCnd != null) { + reader = new InputStreamReader(migrationCnd.openStream()); + CndImporter.registerNodeTypes(reader, session, true); + session.save(); +// log.info("Registered migration node types from " + migrationCnd); + } + + // modify data + dataModification.execute(session); + + // apply changes + session.save(); + + long duration = System.currentTimeMillis() - begin; +// log.info("Migration of data model " + dataModelNodePath + " to " + targetVersion + " performed in " +// + duration + "ms"); + return true; + } catch (RepositoryException e) { + JcrUtils.discardQuietly(session); + throw new JcrException("Migration of data model " + dataModelNodePath + " to " + targetVersion + " failed.", + e); + } catch (ParseException | IOException e) { + JcrUtils.discardQuietly(session); + throw new RuntimeException( + "Migration of data model " + dataModelNodePath + " to " + targetVersion + " failed.", e); + } finally { + JcrUtils.logoutQuietly(session); + IOUtils.closeQuietly(reader); + } + } + + protected static int compareVersions(String version1, String version2) { + // TODO do a proper version analysis and comparison + return version1.compareTo(version2); + } + + /** To be called on a stopped repository. */ + public static void clearRepositoryCaches(RepositoryConfig repositoryConfig) { + try { + String customeNodeTypesPath = "/nodetypes/custom_nodetypes.xml"; + // FIXME causes weird error in Eclipse + repositoryConfig.getFileSystem().deleteFile(customeNodeTypesPath); + if (log.isDebugEnabled()) + log.debug("Cleared " + customeNodeTypesPath); + } catch (RuntimeException e) { + throw e; + } catch (RepositoryException e) { + throw new JcrException(e); + } catch (FileSystemException e) { + throw new RuntimeException("Cannot clear node types cache.",e); + } + + // File customNodeTypes = new File(home.getPath() + // + "/repository/nodetypes/custom_nodetypes.xml"); + // if (customNodeTypes.exists()) { + // customNodeTypes.delete(); + // if (log.isDebugEnabled()) + // log.debug("Cleared " + customNodeTypes); + // } else { + // log.warn("File " + customNodeTypes + " not found."); + // } + } + + /* + * FOR USE IN (SORTED) SETS + */ + + public int compareTo(JackrabbitDataModelMigration dataModelMigration) { + // TODO make ordering smarter + if (dataModelNodePath.equals(dataModelMigration.dataModelNodePath)) + return compareVersions(targetVersion, dataModelMigration.targetVersion); + else + return dataModelNodePath.compareTo(dataModelMigration.dataModelNodePath); + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof JackrabbitDataModelMigration)) + return false; + JackrabbitDataModelMigration dataModelMigration = (JackrabbitDataModelMigration) obj; + return dataModelNodePath.equals(dataModelMigration.dataModelNodePath) + && targetVersion.equals(dataModelMigration.targetVersion); + } + + @Override + public int hashCode() { + return targetVersion.hashCode(); + } + + public void setDataModelNodePath(String dataModelNodePath) { + this.dataModelNodePath = dataModelNodePath; + } + + public void setTargetVersion(String targetVersion) { + this.targetVersion = targetVersion; + } + + public void setMigrationCnd(URL migrationCnd) { + this.migrationCnd = migrationCnd; + } + + public void setDataModification(JcrCallback dataModification) { + this.dataModification = dataModification; + } + + public String getDataModelNodePath() { + return dataModelNodePath; + } + + public String getTargetVersion() { + return targetVersion; + } + +} diff --git a/org.argeo.cms.jcr/src/org/argeo/jackrabbit/client/ClientDavexRepositoryFactory.java b/org.argeo.cms.jcr/src/org/argeo/jackrabbit/client/ClientDavexRepositoryFactory.java new file mode 100644 index 000000000..77ad527e1 --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/jackrabbit/client/ClientDavexRepositoryFactory.java @@ -0,0 +1,26 @@ +package org.argeo.jackrabbit.client; + +import java.util.Map; + +import javax.jcr.Repository; +import javax.jcr.RepositoryException; +import javax.jcr.RepositoryFactory; + +import org.apache.jackrabbit.jcr2spi.Jcr2spiRepositoryFactory; +import org.apache.jackrabbit.jcr2spi.RepositoryImpl; +import org.apache.jackrabbit.spi.RepositoryServiceFactory; + +/** A customised {@link RepositoryFactory} access a remote DAVEX service. */ +public class ClientDavexRepositoryFactory implements RepositoryFactory { + public final static String JACKRABBIT_DAVEX_URI = ClientDavexRepositoryServiceFactory.PARAM_REPOSITORY_URI; + public final static String JACKRABBIT_REMOTE_DEFAULT_WORKSPACE = ClientDavexRepositoryServiceFactory.PARAM_WORKSPACE_NAME_DEFAULT; + + @SuppressWarnings("rawtypes") + @Override + public Repository getRepository(Map parameters) throws RepositoryException { + RepositoryServiceFactory repositoryServiceFactory = new ClientDavexRepositoryServiceFactory(); + return RepositoryImpl + .create(new Jcr2spiRepositoryFactory.RepositoryConfigImpl(repositoryServiceFactory, parameters)); + } + +} diff --git a/org.argeo.cms.jcr/src/org/argeo/jackrabbit/client/ClientDavexRepositoryService.java b/org.argeo.cms.jcr/src/org/argeo/jackrabbit/client/ClientDavexRepositoryService.java new file mode 100644 index 000000000..0f9db8772 --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/jackrabbit/client/ClientDavexRepositoryService.java @@ -0,0 +1,40 @@ +package org.argeo.jackrabbit.client; + +import javax.jcr.RepositoryException; + +import org.apache.http.client.protocol.HttpClientContext; +import org.apache.http.protocol.HttpContext; +import org.apache.jackrabbit.spi.SessionInfo; +import org.apache.jackrabbit.spi2davex.BatchReadConfig; +import org.apache.jackrabbit.spi2davex.RepositoryServiceImpl; + +/** + * Wrapper for {@link RepositoryServiceImpl} in order to access the underlying + * {@link HttpClientContext}. + */ +public class ClientDavexRepositoryService extends RepositoryServiceImpl { + + public ClientDavexRepositoryService(String jcrServerURI, BatchReadConfig batchReadConfig) + throws RepositoryException { + super(jcrServerURI, batchReadConfig); + } + + public ClientDavexRepositoryService(String jcrServerURI, String defaultWorkspaceName, + BatchReadConfig batchReadConfig, int itemInfoCacheSize, int maximumHttpConnections) + throws RepositoryException { + super(jcrServerURI, defaultWorkspaceName, batchReadConfig, itemInfoCacheSize, maximumHttpConnections); + } + + public ClientDavexRepositoryService(String jcrServerURI, String defaultWorkspaceName, + BatchReadConfig batchReadConfig, int itemInfoCacheSize) throws RepositoryException { + super(jcrServerURI, defaultWorkspaceName, batchReadConfig, itemInfoCacheSize); + } + + @Override + protected HttpContext getContext(SessionInfo sessionInfo) throws RepositoryException { + HttpClientContext result = HttpClientContext.create(); + result.setAuthCache(new NonSerialBasicAuthCache()); + return result; + } + +} diff --git a/org.argeo.cms.jcr/src/org/argeo/jackrabbit/client/ClientDavexRepositoryServiceFactory.java b/org.argeo.cms.jcr/src/org/argeo/jackrabbit/client/ClientDavexRepositoryServiceFactory.java new file mode 100644 index 000000000..4b240f060 --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/jackrabbit/client/ClientDavexRepositoryServiceFactory.java @@ -0,0 +1,82 @@ +package org.argeo.jackrabbit.client; + +import java.util.Map; + +import javax.jcr.RepositoryException; + +import org.apache.jackrabbit.spi.RepositoryService; +import org.apache.jackrabbit.spi.commons.ItemInfoCacheImpl; +import org.apache.jackrabbit.spi2davex.BatchReadConfig; +import org.apache.jackrabbit.spi2davex.Spi2davexRepositoryServiceFactory; + +/** + * Wrapper for {@link Spi2davexRepositoryServiceFactory} in order to create a + * {@link ClientDavexRepositoryService}. + */ +public class ClientDavexRepositoryServiceFactory extends Spi2davexRepositoryServiceFactory { + @Override + public RepositoryService createRepositoryService(Map parameters) throws RepositoryException { + // retrieve the repository uri + String uri; + if (parameters == null) { + uri = System.getProperty(PARAM_REPOSITORY_URI); + } else { + Object repoUri = parameters.get(PARAM_REPOSITORY_URI); + uri = (repoUri == null) ? null : repoUri.toString(); + } + if (uri == null) { + uri = DEFAULT_REPOSITORY_URI; + } + + // load other optional configuration parameters + BatchReadConfig brc = null; + int itemInfoCacheSize = ItemInfoCacheImpl.DEFAULT_CACHE_SIZE; + int maximumHttpConnections = 0; + + // since JCR-4120 the default workspace name is no longer set to 'default' + // note: if running with JCR Server < 1.5 a default workspace name must + // therefore be configured + String workspaceNameDefault = null; + + if (parameters != null) { + // batchRead config + Object param = parameters.get(PARAM_BATCHREAD_CONFIG); + if (param != null && param instanceof BatchReadConfig) { + brc = (BatchReadConfig) param; + } + + // itemCache size config + param = parameters.get(PARAM_ITEMINFO_CACHE_SIZE); + if (param != null) { + try { + itemInfoCacheSize = Integer.parseInt(param.toString()); + } catch (NumberFormatException e) { + // ignore, use default + } + } + + // max connections config + param = parameters.get(PARAM_MAX_CONNECTIONS); + if (param != null) { + try { + maximumHttpConnections = Integer.parseInt(param.toString()); + } catch (NumberFormatException e) { + // using default + } + } + + param = parameters.get(PARAM_WORKSPACE_NAME_DEFAULT); + if (param != null) { + workspaceNameDefault = param.toString(); + } + } + + if (maximumHttpConnections > 0) { + return new ClientDavexRepositoryService(uri, workspaceNameDefault, brc, itemInfoCacheSize, + maximumHttpConnections); + } else { + return new ClientDavexRepositoryService(uri, workspaceNameDefault, brc, itemInfoCacheSize); + } + } + +} diff --git a/org.argeo.cms.jcr/src/org/argeo/jackrabbit/client/JackrabbitClient.java b/org.argeo.cms.jcr/src/org/argeo/jackrabbit/client/JackrabbitClient.java new file mode 100644 index 000000000..e08f4d6c7 --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/jackrabbit/client/JackrabbitClient.java @@ -0,0 +1,125 @@ +package org.argeo.jackrabbit.client; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.HashMap; +import java.util.Map; + +import javax.jcr.Node; +import javax.jcr.NodeIterator; +import javax.jcr.Repository; +import javax.jcr.RepositoryException; +import javax.jcr.RepositoryFactory; +import javax.jcr.Session; + +import org.apache.http.client.protocol.HttpClientContext; +import org.apache.http.protocol.HttpContext; +import org.apache.jackrabbit.jcr2dav.Jcr2davRepositoryFactory; +import org.apache.jackrabbit.jcr2spi.Jcr2spiRepositoryFactory; +import org.apache.jackrabbit.jcr2spi.RepositoryImpl; +import org.apache.jackrabbit.spi.RepositoryService; +import org.apache.jackrabbit.spi.RepositoryServiceFactory; +import org.apache.jackrabbit.spi.SessionInfo; +import org.apache.jackrabbit.spi.commons.ItemInfoCacheImpl; +import org.apache.jackrabbit.spi2davex.BatchReadConfig; +import org.apache.jackrabbit.spi2davex.RepositoryServiceImpl; +import org.apache.jackrabbit.spi2davex.Spi2davexRepositoryServiceFactory; +import org.argeo.jcr.JcrUtils; + +/** Minimal client to test JCR DAVEX connectivity. */ +public class JackrabbitClient { + final static String JACKRABBIT_REPOSITORY_URI = "org.apache.jackrabbit.repository.uri"; + final static String JACKRABBIT_DAVEX_URI = "org.apache.jackrabbit.spi2davex.uri"; + final static String JACKRABBIT_REMOTE_DEFAULT_WORKSPACE = "org.apache.jackrabbit.spi2davex.WorkspaceNameDefault"; + + public static void main(String[] args) { + String repoUri = args.length == 0 ? "http://root:demo@localhost:7070/jcr/ego" : args[0]; + String workspace = args.length < 2 ? "home" : args[1]; + + Repository repository = null; + Session session = null; + + URI uri; + try { + uri = new URI(repoUri); + } catch (URISyntaxException e1) { + throw new IllegalArgumentException(e1); + } + + if (uri.getScheme().equals("http") || uri.getScheme().equals("https")) { + + RepositoryFactory repositoryFactory = new Jcr2davRepositoryFactory() { + @SuppressWarnings("rawtypes") + public Repository getRepository(Map parameters) throws RepositoryException { + RepositoryServiceFactory repositoryServiceFactory = new Spi2davexRepositoryServiceFactory() { + + @Override + public RepositoryService createRepositoryService(Map parameters) + throws RepositoryException { + Object uri = parameters.get(JACKRABBIT_DAVEX_URI); + Object defaultWorkspace = parameters.get(JACKRABBIT_REMOTE_DEFAULT_WORKSPACE); + BatchReadConfig brc = null; + return new RepositoryServiceImpl(uri.toString(), defaultWorkspace.toString(), brc, + ItemInfoCacheImpl.DEFAULT_CACHE_SIZE) { + + @Override + protected HttpContext getContext(SessionInfo sessionInfo) throws RepositoryException { + HttpClientContext result = HttpClientContext.create(); + result.setAuthCache(new NonSerialBasicAuthCache()); + return result; + } + + }; + } + }; + return RepositoryImpl.create( + new Jcr2spiRepositoryFactory.RepositoryConfigImpl(repositoryServiceFactory, parameters)); + } + }; + Map params = new HashMap(); + params.put(JACKRABBIT_DAVEX_URI, repoUri.toString()); + // FIXME make it configurable + params.put(JACKRABBIT_REMOTE_DEFAULT_WORKSPACE, "sys"); + + try { + repository = repositoryFactory.getRepository(params); + if (repository != null) + session = repository.login(workspace); + else + throw new IllegalArgumentException("Repository " + repoUri + " not found"); + } catch (RepositoryException e) { + e.printStackTrace(); + } + + } else { + Path path = Paths.get(uri.getPath()); + } + + try { + Node rootNode = session.getRootNode(); + NodeIterator nit = rootNode.getNodes(); + while (nit.hasNext()) { + System.out.println(nit.nextNode().getPath()); + } + + Node newNode = JcrUtils.mkdirs(rootNode, "dir/subdir"); + System.out.println("Created folder " + newNode.getPath()); + Node newFile = JcrUtils.copyBytesAsFile(newNode, "test.txt", "TEST".getBytes()); + System.out.println("Created file " + newFile.getPath()); + try (BufferedReader reader = new BufferedReader(new InputStreamReader(JcrUtils.getFileAsStream(newFile)))) { + System.out.println("Read " + reader.readLine()); + } catch (IOException e) { + e.printStackTrace(); + } + newNode.getParent().remove(); + System.out.println("Removed new nodes"); + } catch (RepositoryException e) { + e.printStackTrace(); + } + } +} diff --git a/org.argeo.cms.jcr/src/org/argeo/jackrabbit/client/NonSerialBasicAuthCache.java b/org.argeo.cms.jcr/src/org/argeo/jackrabbit/client/NonSerialBasicAuthCache.java new file mode 100644 index 000000000..3fb0db9a0 --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/jackrabbit/client/NonSerialBasicAuthCache.java @@ -0,0 +1,41 @@ +package org.argeo.jackrabbit.client; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import org.apache.http.HttpHost; +import org.apache.http.auth.AuthScheme; +import org.apache.http.client.AuthCache; + +/** + * Implementation of {@link AuthCache} which doesn't use serialization, as it is + * not supported by GraalVM at this stage. + */ +public class NonSerialBasicAuthCache implements AuthCache { + private final Map cache; + + public NonSerialBasicAuthCache() { + cache = new ConcurrentHashMap(); + } + + @Override + public void put(HttpHost host, AuthScheme authScheme) { + cache.put(host, authScheme); + } + + @Override + public AuthScheme get(HttpHost host) { + return cache.get(host); + } + + @Override + public void remove(HttpHost host) { + cache.remove(host); + } + + @Override + public void clear() { + cache.clear(); + } + +} diff --git a/org.argeo.cms.jcr/src/org/argeo/jackrabbit/fs/AbstractJackrabbitFsProvider.java b/org.argeo.cms.jcr/src/org/argeo/jackrabbit/fs/AbstractJackrabbitFsProvider.java new file mode 100644 index 000000000..a2eb98302 --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/jackrabbit/fs/AbstractJackrabbitFsProvider.java @@ -0,0 +1,7 @@ +package org.argeo.jackrabbit.fs; + +import org.argeo.jcr.fs.JcrFileSystemProvider; + +public abstract class AbstractJackrabbitFsProvider extends JcrFileSystemProvider { + +} diff --git a/org.argeo.cms.jcr/src/org/argeo/jackrabbit/fs/DavexFsProvider.java b/org.argeo.cms.jcr/src/org/argeo/jackrabbit/fs/DavexFsProvider.java new file mode 100644 index 000000000..1cae6e493 --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/jackrabbit/fs/DavexFsProvider.java @@ -0,0 +1,149 @@ +package org.argeo.jackrabbit.fs; + +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.file.DirectoryStream; +import java.nio.file.FileSystem; +import java.nio.file.FileSystemAlreadyExistsException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.HashMap; +import java.util.Map; + +import javax.jcr.Repository; +import javax.jcr.RepositoryFactory; +import javax.jcr.Session; + +import org.argeo.jackrabbit.client.ClientDavexRepositoryFactory; +import org.argeo.jcr.fs.JcrFileSystem; +import org.argeo.jcr.fs.JcrFsException; + +/** + * A file system provider based on a JCR repository remotely accessed via the + * DAVEX protocol. + */ +public class DavexFsProvider extends AbstractJackrabbitFsProvider { + final static String DEFAULT_JACKRABBIT_REMOTE_DEFAULT_WORKSPACE = "sys"; + + private Map fileSystems = new HashMap<>(); + + @Override + public String getScheme() { + return "davex"; + } + + @Override + public FileSystem newFileSystem(URI uri, Map env) throws IOException { + if (uri.getHost() == null) + throw new IllegalArgumentException("An host should be provided"); + try { + URI repoUri = new URI("http", uri.getUserInfo(), uri.getHost(), uri.getPort(), uri.getPath(), null, null); + String repoKey = repoUri.toString(); + if (fileSystems.containsKey(repoKey)) + throw new FileSystemAlreadyExistsException("CMS file system already exists for " + repoKey); + RepositoryFactory repositoryFactory = new ClientDavexRepositoryFactory(); + return tryGetRepo(repositoryFactory, repoUri, "home"); + } catch (URISyntaxException e) { + throw new IllegalArgumentException("Cannot open file system " + uri, e); + } + } + + private JcrFileSystem tryGetRepo(RepositoryFactory repositoryFactory, URI repoUri, String workspace) + throws IOException { + Map params = new HashMap(); + params.put(ClientDavexRepositoryFactory.JACKRABBIT_DAVEX_URI, repoUri.toString()); + // TODO better integrate with OSGi or other configuration than system + // properties. + String remoteDefaultWorkspace = System.getProperty( + ClientDavexRepositoryFactory.JACKRABBIT_REMOTE_DEFAULT_WORKSPACE, + DEFAULT_JACKRABBIT_REMOTE_DEFAULT_WORKSPACE); + params.put(ClientDavexRepositoryFactory.JACKRABBIT_REMOTE_DEFAULT_WORKSPACE, remoteDefaultWorkspace); + Repository repository = null; + Session session = null; + try { + repository = repositoryFactory.getRepository(params); + if (repository != null) + session = repository.login(workspace); + } catch (Exception e) { + // silent + } + + if (session == null) { + if (repoUri.getPath() == null || repoUri.getPath().equals("/")) + return null; + String repoUriStr = repoUri.toString(); + if (repoUriStr.endsWith("/")) + repoUriStr = repoUriStr.substring(0, repoUriStr.length() - 1); + String nextRepoUriStr = repoUriStr.substring(0, repoUriStr.lastIndexOf('/')); + String nextWorkspace = repoUriStr.substring(repoUriStr.lastIndexOf('/') + 1); + URI nextUri; + try { + nextUri = new URI(nextRepoUriStr); + } catch (URISyntaxException e) { + throw new IllegalArgumentException("Badly formatted URI", e); + } + return tryGetRepo(repositoryFactory, nextUri, nextWorkspace); + } else { + JcrFileSystem fileSystem = new JcrFileSystem(this, repository); + fileSystems.put(repoUri.toString() + "/" + workspace, fileSystem); + return fileSystem; + } + } + + @Override + public FileSystem getFileSystem(URI uri) { + return currentUserFileSystem(uri); + } + + @Override + public Path getPath(URI uri) { + JcrFileSystem fileSystem = currentUserFileSystem(uri); + if (fileSystem == null) + try { + fileSystem = (JcrFileSystem) newFileSystem(uri, new HashMap()); + if (fileSystem == null) + throw new IllegalArgumentException("No file system found for " + uri); + } catch (IOException e) { + throw new JcrFsException("Could not autocreate file system", e); + } + URI repoUri = null; + try { + repoUri = new URI("http", uri.getUserInfo(), uri.getHost(), uri.getPort(), uri.getPath(), null, null); + } catch (URISyntaxException e) { + throw new IllegalArgumentException(e); + } + String uriStr = repoUri.toString(); + String localPath = null; + for (String key : fileSystems.keySet()) { + if (uriStr.startsWith(key)) { + localPath = uriStr.toString().substring(key.length()); + } + } + if ("".equals(localPath)) + localPath = "/"; + return fileSystem.getPath(localPath); + } + + private JcrFileSystem currentUserFileSystem(URI uri) { + for (String key : fileSystems.keySet()) { + if (uri.toString().startsWith(key)) + return fileSystems.get(key); + } + return null; + } + + public static void main(String args[]) { + try { + DavexFsProvider fsProvider = new DavexFsProvider(); + Path path = fsProvider.getPath(new URI("davex://root:demo@localhost:7070/jcr/ego/")); + System.out.println(path); + DirectoryStream ds = Files.newDirectoryStream(path); + for (Path p : ds) { + System.out.println("- " + p); + } + } catch (Exception e) { + e.printStackTrace(); + } + } +} diff --git a/org.argeo.cms.jcr/src/org/argeo/jackrabbit/fs/JackrabbitMemoryFsProvider.java b/org.argeo.cms.jcr/src/org/argeo/jackrabbit/fs/JackrabbitMemoryFsProvider.java new file mode 100644 index 000000000..e3a70d084 --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/jackrabbit/fs/JackrabbitMemoryFsProvider.java @@ -0,0 +1,87 @@ +package org.argeo.jackrabbit.fs; + +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.nio.file.FileSystem; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.HashMap; +import java.util.Map; + +import javax.jcr.Credentials; +import javax.jcr.Repository; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.SimpleCredentials; + +import org.apache.jackrabbit.core.RepositoryImpl; +import org.apache.jackrabbit.core.config.RepositoryConfig; +import org.argeo.jcr.fs.JcrFileSystem; +import org.argeo.jcr.fs.JcrFsException; + +public class JackrabbitMemoryFsProvider extends AbstractJackrabbitFsProvider { + private RepositoryImpl repository; + private JcrFileSystem fileSystem; + + private Credentials credentials; + + public JackrabbitMemoryFsProvider() { + String username = System.getProperty("user.name"); + credentials = new SimpleCredentials(username, username.toCharArray()); + } + + @Override + public String getScheme() { + return "jcr+memory"; + } + + @Override + public FileSystem newFileSystem(URI uri, Map env) throws IOException { + try { + Path tempDir = Files.createTempDirectory("fs-memory"); + URL confUrl = JackrabbitMemoryFsProvider.class.getResource("fs-memory.xml"); + RepositoryConfig repositoryConfig = RepositoryConfig.create(confUrl.toURI(), tempDir.toString()); + repository = RepositoryImpl.create(repositoryConfig); + postRepositoryCreation(repository); + fileSystem = new JcrFileSystem(this, repository, credentials); + return fileSystem; + } catch (RepositoryException | URISyntaxException e) { + throw new IOException("Cannot login to repository", e); + } + } + + @Override + public FileSystem getFileSystem(URI uri) { + return fileSystem; + } + + @Override + public Path getPath(URI uri) { + String path = uri.getPath(); + if (fileSystem == null) + try { + newFileSystem(uri, new HashMap()); + } catch (IOException e) { + throw new JcrFsException("Could not autocreate file system", e); + } + return fileSystem.getPath(path); + } + + public Repository getRepository() { + return repository; + } + + public Session login() throws RepositoryException { + return getRepository().login(credentials); + } + + /** + * Called after the repository has been created and before the file system is + * created. + */ + protected void postRepositoryCreation(RepositoryImpl repositoryImpl) throws RepositoryException { + + } +} diff --git a/org.argeo.cms.jcr/src/org/argeo/jackrabbit/fs/fs-memory.xml b/org.argeo.cms.jcr/src/org/argeo/jackrabbit/fs/fs-memory.xml new file mode 100644 index 000000000..f2541fb4e --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/jackrabbit/fs/fs-memory.xml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/org.argeo.cms.jcr/src/org/argeo/jackrabbit/fs/package-info.java b/org.argeo.cms.jcr/src/org/argeo/jackrabbit/fs/package-info.java new file mode 100644 index 000000000..c9ec2c3b9 --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/jackrabbit/fs/package-info.java @@ -0,0 +1,2 @@ +/** Java NIO file system implementation based on Jackrabbit. */ +package org.argeo.jackrabbit.fs; \ No newline at end of file diff --git a/org.argeo.cms.jcr/src/org/argeo/jackrabbit/package-info.java b/org.argeo.cms.jcr/src/org/argeo/jackrabbit/package-info.java new file mode 100644 index 000000000..17497d62c --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/jackrabbit/package-info.java @@ -0,0 +1,2 @@ +/** Generic Jackrabbit utilities. */ +package org.argeo.jackrabbit; \ No newline at end of file diff --git a/org.argeo.cms.jcr/src/org/argeo/jackrabbit/repository-h2.xml b/org.argeo.cms.jcr/src/org/argeo/jackrabbit/repository-h2.xml new file mode 100644 index 000000000..05267621f --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/jackrabbit/repository-h2.xml @@ -0,0 +1,82 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/org.argeo.cms.jcr/src/org/argeo/jackrabbit/repository-localfs.xml b/org.argeo.cms.jcr/src/org/argeo/jackrabbit/repository-localfs.xml new file mode 100644 index 000000000..3d2470863 --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/jackrabbit/repository-localfs.xml @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/org.argeo.cms.jcr/src/org/argeo/jackrabbit/repository-memory.xml b/org.argeo.cms.jcr/src/org/argeo/jackrabbit/repository-memory.xml new file mode 100644 index 000000000..ecee5bdad --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/jackrabbit/repository-memory.xml @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/org.argeo.cms.jcr/src/org/argeo/jackrabbit/repository-postgresql-ds.xml b/org.argeo.cms.jcr/src/org/argeo/jackrabbit/repository-postgresql-ds.xml new file mode 100644 index 000000000..07a0d0428 --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/jackrabbit/repository-postgresql-ds.xml @@ -0,0 +1,82 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/org.argeo.cms.jcr/src/org/argeo/jackrabbit/repository-postgresql.xml b/org.argeo.cms.jcr/src/org/argeo/jackrabbit/repository-postgresql.xml new file mode 100644 index 000000000..967782820 --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/jackrabbit/repository-postgresql.xml @@ -0,0 +1,79 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/org.argeo.cms.jcr/src/org/argeo/jackrabbit/security/JackrabbitSecurityUtils.java b/org.argeo.cms.jcr/src/org/argeo/jackrabbit/security/JackrabbitSecurityUtils.java new file mode 100644 index 000000000..a75c79541 --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/jackrabbit/security/JackrabbitSecurityUtils.java @@ -0,0 +1,80 @@ +package org.argeo.jackrabbit.security; + +import java.security.Principal; +import java.util.ArrayList; +import java.util.List; + +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.security.Privilege; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.jackrabbit.api.security.JackrabbitAccessControlList; +import org.apache.jackrabbit.api.security.JackrabbitAccessControlManager; +import org.argeo.jcr.JcrUtils; + +/** Utilities around Jackrabbit security extensions. */ +public class JackrabbitSecurityUtils { + private final static Log log = LogFactory.getLog(JackrabbitSecurityUtils.class); + + /** + * Convenience method for denying a single privilege to a principal (user or + * role), typically jcr:all + */ + public synchronized static void denyPrivilege(Session session, String path, String principal, String privilege) + throws RepositoryException { + List privileges = new ArrayList(); + privileges.add(session.getAccessControlManager().privilegeFromName(privilege)); + denyPrivileges(session, path, () -> principal, privileges); + } + + /** + * Deny privileges on a path to a {@link Principal}. The path must already + * exist. Session is saved. Synchronized to prevent concurrent modifications of + * the same node. + */ + public synchronized static Boolean denyPrivileges(Session session, String path, Principal principal, + List privs) throws RepositoryException { + // make sure the session is in line with the persisted state + session.refresh(false); + JackrabbitAccessControlManager acm = (JackrabbitAccessControlManager) session.getAccessControlManager(); + JackrabbitAccessControlList acl = (JackrabbitAccessControlList) JcrUtils.getAccessControlList(acm, path); + +// accessControlEntries: for (AccessControlEntry ace : acl.getAccessControlEntries()) { +// Principal currentPrincipal = ace.getPrincipal(); +// if (currentPrincipal.getName().equals(principal.getName())) { +// Privilege[] currentPrivileges = ace.getPrivileges(); +// if (currentPrivileges.length != privs.size()) +// break accessControlEntries; +// for (int i = 0; i < currentPrivileges.length; i++) { +// Privilege currP = currentPrivileges[i]; +// Privilege p = privs.get(i); +// if (!currP.getName().equals(p.getName())) { +// break accessControlEntries; +// } +// } +// return false; +// } +// } + + Privilege[] privileges = privs.toArray(new Privilege[privs.size()]); + acl.addEntry(principal, privileges, false); + acm.setPolicy(path, acl); + if (log.isDebugEnabled()) { + StringBuffer privBuf = new StringBuffer(); + for (Privilege priv : privs) + privBuf.append(priv.getName()); + log.debug("Denied privileges " + privBuf + " to " + principal.getName() + " on " + path + " in '" + + session.getWorkspace().getName() + "'"); + } + session.refresh(true); + session.save(); + return true; + } + + /** Singleton. */ + private JackrabbitSecurityUtils() { + + } +} diff --git a/org.argeo.cms.jcr/src/org/argeo/jackrabbit/security/package-info.java b/org.argeo.cms.jcr/src/org/argeo/jackrabbit/security/package-info.java new file mode 100644 index 000000000..f3a282c4e --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/jackrabbit/security/package-info.java @@ -0,0 +1,2 @@ +/** Generic Jackrabbit security utilities. */ +package org.argeo.jackrabbit.security; \ No newline at end of file diff --git a/org.argeo.cms.jcr/src/org/argeo/jackrabbit/unit/AbstractJackrabbitTestCase.java b/org.argeo.cms.jcr/src/org/argeo/jackrabbit/unit/AbstractJackrabbitTestCase.java new file mode 100644 index 000000000..f65432eb7 --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/jackrabbit/unit/AbstractJackrabbitTestCase.java @@ -0,0 +1,51 @@ +package org.argeo.jackrabbit.unit; + +import java.net.URL; + +import javax.jcr.Repository; + +import org.apache.commons.io.FileUtils; +import org.apache.jackrabbit.core.RepositoryImpl; +import org.apache.jackrabbit.core.config.RepositoryConfig; +import org.argeo.jcr.unit.AbstractJcrTestCase; + +/** Factorizes configuration of an in memory transient repository */ +public abstract class AbstractJackrabbitTestCase extends AbstractJcrTestCase { + protected RepositoryImpl repositoryImpl; + + // protected File getRepositoryFile() throws Exception { + // Resource res = new ClassPathResource( + // "org/argeo/jackrabbit/unit/repository-memory.xml"); + // return res.getFile(); + // } + + public AbstractJackrabbitTestCase() { + URL url = AbstractJackrabbitTestCase.class.getResource("jaas.config"); + assert url != null; + System.setProperty("java.security.auth.login.config", url.toString()); + } + + protected Repository createRepository() throws Exception { + // Repository repository = new TransientRepository(getRepositoryFile(), + // getHomeDir()); + RepositoryConfig repositoryConfig = RepositoryConfig.create( + AbstractJackrabbitTestCase.class + .getResourceAsStream(getRepositoryConfigResource()), + getHomeDir().getAbsolutePath()); + RepositoryImpl repositoryImpl = RepositoryImpl.create(repositoryConfig); + return repositoryImpl; + } + + protected String getRepositoryConfigResource() { + return "repository-memory.xml"; + } + + @Override + protected void clearRepository(Repository repository) throws Exception { + RepositoryImpl repositoryImpl = (RepositoryImpl) repository; + if (repositoryImpl != null) + repositoryImpl.shutdown(); + FileUtils.deleteDirectory(getHomeDir()); + } + +} diff --git a/org.argeo.cms.jcr/src/org/argeo/jackrabbit/unit/jaas.config b/org.argeo.cms.jcr/src/org/argeo/jackrabbit/unit/jaas.config new file mode 100644 index 000000000..0313f91e5 --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/jackrabbit/unit/jaas.config @@ -0,0 +1,7 @@ +TEST_JACKRABBIT_ADMIN { + org.argeo.cms.auth.DataAdminLoginModule requisite; +}; + +Jackrabbit { + org.argeo.security.jackrabbit.SystemJackrabbitLoginModule requisite; +}; diff --git a/org.argeo.cms.jcr/src/org/argeo/jackrabbit/unit/package-info.java b/org.argeo.cms.jcr/src/org/argeo/jackrabbit/unit/package-info.java new file mode 100644 index 000000000..3b6143b34 --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/jackrabbit/unit/package-info.java @@ -0,0 +1,2 @@ +/** Helpers for unit tests with Jackrabbit repositories. */ +package org.argeo.jackrabbit.unit; \ No newline at end of file diff --git a/org.argeo.cms.jcr/src/org/argeo/jackrabbit/unit/repository-h2.xml b/org.argeo.cms.jcr/src/org/argeo/jackrabbit/unit/repository-h2.xml new file mode 100644 index 000000000..348dc288b --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/jackrabbit/unit/repository-h2.xml @@ -0,0 +1,81 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/org.argeo.cms.jcr/src/org/argeo/jackrabbit/unit/repository-memory.xml b/org.argeo.cms.jcr/src/org/argeo/jackrabbit/unit/repository-memory.xml new file mode 100644 index 000000000..839542417 --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/jackrabbit/unit/repository-memory.xml @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/org.argeo.cms.jcr/src/org/argeo/jcr/Bin.java b/org.argeo.cms.jcr/src/org/argeo/jcr/Bin.java new file mode 100644 index 000000000..0418810ed --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/jcr/Bin.java @@ -0,0 +1,60 @@ +package org.argeo.jcr; + +import java.io.IOException; +import java.io.InputStream; + +import javax.jcr.Binary; +import javax.jcr.Property; +import javax.jcr.RepositoryException; + +/** + * A {@link Binary} wrapper implementing {@link AutoCloseable} for ease of use + * in try/catch blocks. + */ +public class Bin implements Binary, AutoCloseable { + private final Binary wrappedBinary; + + public Bin(Property property) throws RepositoryException { + this(property.getBinary()); + } + + public Bin(Binary wrappedBinary) { + if (wrappedBinary == null) + throw new IllegalArgumentException("Wrapped binary cannot be null"); + this.wrappedBinary = wrappedBinary; + } + + // private static Binary getBinary(Property property) throws IOException { + // try { + // return property.getBinary(); + // } catch (RepositoryException e) { + // throw new IOException("Cannot get binary from property " + property, e); + // } + // } + + @Override + public void close() { + dispose(); + } + + @Override + public InputStream getStream() throws RepositoryException { + return wrappedBinary.getStream(); + } + + @Override + public int read(byte[] b, long position) throws IOException, RepositoryException { + return wrappedBinary.read(b, position); + } + + @Override + public long getSize() throws RepositoryException { + return wrappedBinary.getSize(); + } + + @Override + public void dispose() { + wrappedBinary.dispose(); + } + +} diff --git a/org.argeo.cms.jcr/src/org/argeo/jcr/CollectionNodeIterator.java b/org.argeo.cms.jcr/src/org/argeo/jcr/CollectionNodeIterator.java new file mode 100644 index 000000000..b4124eea5 --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/jcr/CollectionNodeIterator.java @@ -0,0 +1,61 @@ +package org.argeo.jcr; + +import java.util.Collection; +import java.util.Iterator; +import java.util.NoSuchElementException; + +import javax.jcr.Node; +import javax.jcr.NodeIterator; + +/** Wraps a collection of nodes in order to read it as a {@link NodeIterator} */ +public class CollectionNodeIterator implements NodeIterator { + private final Long collectionSize; + private final Iterator iterator; + private Integer position = 0; + + public CollectionNodeIterator(Collection nodes) { + super(); + this.collectionSize = (long) nodes.size(); + this.iterator = nodes.iterator(); + } + + public void skip(long skipNum) { + if (skipNum < 0) + throw new IllegalArgumentException( + "Skip count has to be positive: " + skipNum); + + for (long i = 0; i < skipNum; i++) { + if (!hasNext()) + throw new NoSuchElementException("Last element past (position=" + + getPosition() + ")"); + nextNode(); + } + } + + public long getSize() { + return collectionSize; + } + + public long getPosition() { + return position; + } + + public boolean hasNext() { + return iterator.hasNext(); + } + + public Object next() { + return nextNode(); + } + + public void remove() { + iterator.remove(); + } + + public Node nextNode() { + Node node = iterator.next(); + position++; + return node; + } + +} diff --git a/org.argeo.cms.jcr/src/org/argeo/jcr/DefaultJcrListener.java b/org.argeo.cms.jcr/src/org/argeo/jcr/DefaultJcrListener.java new file mode 100644 index 000000000..fc6888851 --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/jcr/DefaultJcrListener.java @@ -0,0 +1,78 @@ +package org.argeo.jcr; + +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.observation.Event; +import javax.jcr.observation.EventIterator; +import javax.jcr.observation.EventListener; +import javax.jcr.observation.ObservationManager; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** To be overridden */ +public class DefaultJcrListener implements EventListener { + private final static Log log = LogFactory.getLog(DefaultJcrListener.class); + private Session session; + private String path = "/"; + private Boolean deep = true; + + public void start() { + try { + addEventListener(session().getWorkspace().getObservationManager()); + if (log.isDebugEnabled()) + log.debug("Registered JCR event listener on " + path); + } catch (RepositoryException e) { + throw new JcrException("Cannot register event listener", e); + } + } + + public void stop() { + try { + session().getWorkspace().getObservationManager() + .removeEventListener(this); + if (log.isDebugEnabled()) + log.debug("Unregistered JCR event listener on " + path); + } catch (RepositoryException e) { + throw new JcrException("Cannot unregister event listener", e); + } + } + + /** Default is listen to all events */ + protected Integer getEvents() { + return Event.NODE_ADDED | Event.NODE_REMOVED | Event.PROPERTY_ADDED + | Event.PROPERTY_CHANGED | Event.PROPERTY_REMOVED; + } + + /** To be overidden */ + public void onEvent(EventIterator events) { + while (events.hasNext()) { + Event event = events.nextEvent(); + log.debug(event); + } + } + + /** To be overidden */ + protected void addEventListener(ObservationManager observationManager) + throws RepositoryException { + observationManager.addEventListener(this, getEvents(), path, deep, + null, null, false); + } + + private Session session() { + return session; + } + + public void setPath(String path) { + this.path = path; + } + + public void setDeep(Boolean deep) { + this.deep = deep; + } + + public void setSession(Session session) { + this.session = session; + } + +} diff --git a/org.argeo.cms.jcr/src/org/argeo/jcr/Jcr.java b/org.argeo.cms.jcr/src/org/argeo/jcr/Jcr.java new file mode 100644 index 000000000..72e325d35 --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/jcr/Jcr.java @@ -0,0 +1,975 @@ +package org.argeo.jcr; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.math.BigDecimal; +import java.text.MessageFormat; +import java.time.Instant; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Calendar; +import java.util.Collections; +import java.util.Date; +import java.util.GregorianCalendar; +import java.util.Iterator; +import java.util.List; + +import javax.jcr.Binary; +import javax.jcr.ItemNotFoundException; +import javax.jcr.Node; +import javax.jcr.NodeIterator; +import javax.jcr.Property; +import javax.jcr.PropertyType; +import javax.jcr.Repository; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.Value; +import javax.jcr.Workspace; +import javax.jcr.nodetype.NodeType; +import javax.jcr.query.Query; +import javax.jcr.query.QueryManager; +import javax.jcr.security.Privilege; +import javax.jcr.version.Version; +import javax.jcr.version.VersionHistory; +import javax.jcr.version.VersionIterator; +import javax.jcr.version.VersionManager; + +import org.apache.commons.io.IOUtils; + +/** + * Utility class whose purpose is to make using JCR less verbose by + * systematically using unchecked exceptions and returning null + * when something is not found. This is especially useful when writing user + * interfaces (such as with SWT) where listeners and callbacks expect unchecked + * exceptions. Loosely inspired by Java's Files singleton. + */ +public class Jcr { + /** + * The name of a node which will be serialized as XML text, as per section 7.3.1 + * of the JCR 2.0 specifications. + */ + public final static String JCR_XMLTEXT = "jcr:xmltext"; + /** + * The name of a property which will be serialized as XML text, as per section + * 7.3.1 of the JCR 2.0 specifications. + */ + public final static String JCR_XMLCHARACTERS = "jcr:xmlcharacters"; + /** + * jcr:name, when used in another context than + * {@link Property#JCR_NAME}, typically to name a node rather than a property. + */ + public final static String JCR_NAME = "jcr:name"; + /** + * jcr:path, when used in another context than + * {@link Property#JCR_PATH}, typically to name a node rather than a property. + */ + public final static String JCR_PATH = "jcr:path"; + /** + * jcr:primaryType with prefix instead of namespace (as in + * {@link Property#JCR_PRIMARY_TYPE}. + */ + public final static String JCR_PRIMARY_TYPE = "jcr:primaryType"; + /** + * jcr:mixinTypes with prefix instead of namespace (as in + * {@link Property#JCR_MIXIN_TYPES}. + */ + public final static String JCR_MIXIN_TYPES = "jcr:mixinTypes"; + /** + * jcr:uuid with prefix instead of namespace (as in + * {@link Property#JCR_UUID}. + */ + public final static String JCR_UUID = "jcr:uuid"; + /** + * jcr:created with prefix instead of namespace (as in + * {@link Property#JCR_CREATED}. + */ + public final static String JCR_CREATED = "jcr:created"; + /** + * jcr:createdBy with prefix instead of namespace (as in + * {@link Property#JCR_CREATED_BY}. + */ + public final static String JCR_CREATED_BY = "jcr:createdBy"; + /** + * jcr:lastModified with prefix instead of namespace (as in + * {@link Property#JCR_LAST_MODIFIED}. + */ + public final static String JCR_LAST_MODIFIED = "jcr:lastModified"; + /** + * jcr:lastModifiedBy with prefix instead of namespace (as in + * {@link Property#JCR_LAST_MODIFIED_BY}. + */ + public final static String JCR_LAST_MODIFIED_BY = "jcr:lastModifiedBy"; + + /** + * @see Node#isNodeType(String) + * @throws JcrException caused by {@link RepositoryException} + */ + public static boolean isNodeType(Node node, String nodeTypeName) { + try { + return node.isNodeType(nodeTypeName); + } catch (RepositoryException e) { + throw new JcrException("Cannot get whether " + node + " is of type " + nodeTypeName, e); + } + } + + /** + * @see Node#hasNodes() + * @throws JcrException caused by {@link RepositoryException} + */ + public static boolean hasNodes(Node node) { + try { + return node.hasNodes(); + } catch (RepositoryException e) { + throw new JcrException("Cannot get whether " + node + " has children.", e); + } + } + + /** + * @see Node#getParent() + * @throws JcrException caused by {@link RepositoryException} + */ + public static Node getParent(Node node) { + try { + return isRoot(node) ? null : node.getParent(); + } catch (RepositoryException e) { + throw new JcrException("Cannot get parent of " + node, e); + } + } + + /** + * @see Node#getParent() + * @throws JcrException caused by {@link RepositoryException} + */ + public static String getParentPath(Node node) { + return getPath(getParent(node)); + } + + /** + * Whether this node is the root node. + * + * @throws JcrException caused by {@link RepositoryException} + */ + public static boolean isRoot(Node node) { + try { + return node.getDepth() == 0; + } catch (RepositoryException e) { + throw new JcrException("Cannot get depth of " + node, e); + } + } + + /** + * @see Node#getPath() + * @throws JcrException caused by {@link RepositoryException} + */ + public static String getPath(Node node) { + try { + return node.getPath(); + } catch (RepositoryException e) { + throw new JcrException("Cannot get path of " + node, e); + } + } + + /** + * @see Node#getSession() + * @see Session#getWorkspace() + * @see Workspace#getName() + */ + public static String getWorkspaceName(Node node) { + return session(node).getWorkspace().getName(); + } + + /** + * @see Node#getIdentifier() + * @throws JcrException caused by {@link RepositoryException} + */ + public static String getIdentifier(Node node) { + try { + return node.getIdentifier(); + } catch (RepositoryException e) { + throw new JcrException("Cannot get identifier of " + node, e); + } + } + + /** + * @see Node#getName() + * @throws JcrException caused by {@link RepositoryException} + */ + public static String getName(Node node) { + try { + return node.getName(); + } catch (RepositoryException e) { + throw new JcrException("Cannot get name of " + node, e); + } + } + + /** + * Returns the node name with its current index (useful for re-ordering). + * + * @see Node#getName() + * @see Node#getIndex() + * @throws JcrException caused by {@link RepositoryException} + */ + public static String getIndexedName(Node node) { + try { + return node.getName() + "[" + node.getIndex() + "]"; + } catch (RepositoryException e) { + throw new JcrException("Cannot get name of " + node, e); + } + } + + /** + * @see Node#getProperty(String) + * @throws JcrException caused by {@link RepositoryException} + */ + public static Property getProperty(Node node, String property) { + try { + if (node.hasProperty(property)) + return node.getProperty(property); + else + return null; + } catch (RepositoryException e) { + throw new JcrException("Cannot get property " + property + " of " + node, e); + } + } + + /** + * @see Node#getIndex() + * @throws JcrException caused by {@link RepositoryException} + */ + public static int getIndex(Node node) { + try { + return node.getIndex(); + } catch (RepositoryException e) { + throw new JcrException("Cannot get index of " + node, e); + } + } + + /** + * If node has mixin {@link NodeType#MIX_TITLE}, return + * {@link Property#JCR_TITLE}, otherwise return {@link #getName(Node)}. + */ + public static String getTitle(Node node) { + if (Jcr.isNodeType(node, NodeType.MIX_TITLE)) + return get(node, Property.JCR_TITLE); + else + return Jcr.getName(node); + } + + /** Accesses a {@link NodeIterator} as an {@link Iterable}. */ + @SuppressWarnings("unchecked") + public static Iterable iterate(NodeIterator nodeIterator) { + return new Iterable() { + + @Override + public Iterator iterator() { + return nodeIterator; + } + }; + } + + /** + * @return the children as an {@link Iterable} for use in for-each llops. + * @see Node#getNodes() + * @throws JcrException caused by {@link RepositoryException} + */ + public static Iterable nodes(Node node) { + try { + return iterate(node.getNodes()); + } catch (RepositoryException e) { + throw new JcrException("Cannot get children of " + node, e); + } + } + + /** + * @return the children as a (possibly empty) {@link List}. + * @see Node#getNodes() + * @throws JcrException caused by {@link RepositoryException} + */ + public static List getNodes(Node node) { + List nodes = new ArrayList<>(); + try { + if (node.hasNodes()) { + NodeIterator nit = node.getNodes(); + while (nit.hasNext()) + nodes.add(nit.nextNode()); + return nodes; + } else + return nodes; + } catch (RepositoryException e) { + throw new JcrException("Cannot get children of " + node, e); + } + } + + /** + * @return the child or null if not found + * @see Node#getNode(String) + * @throws JcrException caused by {@link RepositoryException} + */ + public static Node getNode(Node node, String child) { + try { + if (node.hasNode(child)) + return node.getNode(child); + else + return null; + } catch (RepositoryException e) { + throw new JcrException("Cannot get child of " + node, e); + } + } + + /** + * @return the node at this path or null if not found + * @see Session#getNode(String) + * @throws JcrException caused by {@link RepositoryException} + */ + public static Node getNode(Session session, String path) { + try { + if (session.nodeExists(path)) + return session.getNode(path); + else + return null; + } catch (RepositoryException e) { + throw new JcrException("Cannot get node " + path, e); + } + } + + /** + * Add a node to this parent, setting its primary type and its mixins. + * + * @param parent the parent node + * @param name the name of the node, if null, the primary + * type will be used (typically for XML structures) + * @param primaryType the primary type, if null + * {@link NodeType#NT_UNSTRUCTURED} will be used. + * @param mixins the mixins + * @return the created node + * @see Node#addNode(String, String) + * @see Node#addMixin(String) + */ + public static Node addNode(Node parent, String name, String primaryType, String... mixins) { + if (name == null && primaryType == null) + throw new IllegalArgumentException("Both node name and primary type cannot be null"); + try { + Node newNode = parent.addNode(name == null ? primaryType : name, + primaryType == null ? NodeType.NT_UNSTRUCTURED : primaryType); + for (String mixin : mixins) { + newNode.addMixin(mixin); + } + return newNode; + } catch (RepositoryException e) { + throw new JcrException("Cannot add node " + name + " to " + parent, e); + } + } + + /** + * Add an {@link NodeType#NT_BASE} node to this parent. + * + * @param parent the parent node + * @param name the name of the node, cannot be null + * @return the created node + * + * @see Node#addNode(String) + */ + public static Node addNode(Node parent, String name) { + if (name == null) + throw new IllegalArgumentException("Node name cannot be null"); + try { + Node newNode = parent.addNode(name); + return newNode; + } catch (RepositoryException e) { + throw new JcrException("Cannot add node " + name + " to " + parent, e); + } + } + + /** + * Add mixins to a node. + * + * @param node the node + * @param mixins the mixins + * @return the created node + * @see Node#addMixin(String) + */ + public static void addMixin(Node node, String... mixins) { + try { + for (String mixin : mixins) { + node.addMixin(mixin); + } + } catch (RepositoryException e) { + throw new JcrException("Cannot add mixins " + Arrays.asList(mixins) + " to " + node, e); + } + } + + /** + * Removes this node. + * + * @see Node#remove() + */ + public static void remove(Node node) { + try { + node.remove(); + } catch (RepositoryException e) { + throw new JcrException("Cannot remove node " + node, e); + } + } + + /** + * @return the node with htis id or null if not found + * @see Session#getNodeByIdentifier(String) + * @throws JcrException caused by {@link RepositoryException} + */ + public static Node getNodeById(Session session, String id) { + try { + return session.getNodeByIdentifier(id); + } catch (ItemNotFoundException e) { + return null; + } catch (RepositoryException e) { + throw new JcrException("Cannot get node with id " + id, e); + } + } + + /** + * Set a property to the given value, or remove it if the value is + * null. + * + * @throws JcrException caused by {@link RepositoryException} + */ + public static void set(Node node, String property, Object value) { + try { + if (!node.hasProperty(property)) { + if (value != null) { + if (value instanceof List) {// multiple + List lst = (List) value; + String[] values = new String[lst.size()]; + for (int i = 0; i < lst.size(); i++) { + values[i] = lst.get(i).toString(); + } + node.setProperty(property, values); + } else { + node.setProperty(property, value.toString()); + } + } + return; + } + Property prop = node.getProperty(property); + if (value == null) { + prop.remove(); + return; + } + + // multiple + if (value instanceof List) { + List lst = (List) value; + String[] values = new String[lst.size()]; + // TODO better cast? + for (int i = 0; i < lst.size(); i++) { + values[i] = lst.get(i).toString(); + } + if (!prop.isMultiple()) + prop.remove(); + node.setProperty(property, values); + return; + } + + // single + if (prop.isMultiple()) { + prop.remove(); + node.setProperty(property, value.toString()); + return; + } + + if (value instanceof String) + prop.setValue((String) value); + else if (value instanceof Long) + prop.setValue((Long) value); + else if (value instanceof Integer) + prop.setValue(((Integer) value).longValue()); + else if (value instanceof Double) + prop.setValue((Double) value); + else if (value instanceof Float) + prop.setValue(((Float) value).doubleValue()); + else if (value instanceof Calendar) + prop.setValue((Calendar) value); + else if (value instanceof BigDecimal) + prop.setValue((BigDecimal) value); + else if (value instanceof Boolean) + prop.setValue((Boolean) value); + else if (value instanceof byte[]) + JcrUtils.setBinaryAsBytes(prop, (byte[]) value); + else if (value instanceof Instant) { + Instant instant = (Instant) value; + GregorianCalendar calendar = new GregorianCalendar(); + calendar.setTime(Date.from(instant)); + prop.setValue(calendar); + } else // try with toString() + prop.setValue(value.toString()); + } catch (RepositoryException e) { + throw new JcrException("Cannot set property " + property + " of " + node + " to " + value, e); + } + } + + /** + * Get property as {@link String}. + * + * @return the value of + * {@link Node#getProperty(String)}.{@link Property#getString()} or + * null if the property does not exist. + * @throws JcrException caused by {@link RepositoryException} + */ + public static String get(Node node, String property) { + return get(node, property, null); + } + + /** + * Get property as a {@link String}. If the property is multiple it returns the + * first value. + * + * @return the value of + * {@link Node#getProperty(String)}.{@link Property#getString()} or + * defaultValue if the property does not exist. + * @throws JcrException caused by {@link RepositoryException} + */ + public static String get(Node node, String property, String defaultValue) { + try { + if (node.hasProperty(property)) { + Property p = node.getProperty(property); + if (!p.isMultiple()) + return p.getString(); + else { + Value[] values = p.getValues(); + if (values.length == 0) + return defaultValue; + else + return values[0].getString(); + } + } else + return defaultValue; + } catch (RepositoryException e) { + throw new JcrException("Cannot retrieve property " + property + " from " + node, e); + } + } + + /** + * Get property as a {@link Value}. + * + * @return {@link Node#getProperty(String)} or null if the property + * does not exist. + * @throws JcrException caused by {@link RepositoryException} + */ + public static Value getValue(Node node, String property) { + try { + if (node.hasProperty(property)) + return node.getProperty(property).getValue(); + else + return null; + } catch (RepositoryException e) { + throw new JcrException("Cannot retrieve property " + property + " from " + node, e); + } + } + + /** + * Get property doing a best effort to cast it as the target object. + * + * @return the value of {@link Node#getProperty(String)} or + * defaultValue if the property does not exist. + * @throws IllegalArgumentException if the value could not be cast + * @throws JcrException in case of unexpected + * {@link RepositoryException} + */ + @SuppressWarnings("unchecked") + public static T getAs(Node node, String property, T defaultValue) { + try { + // TODO deal with multiple + if (node.hasProperty(property)) { + Property p = node.getProperty(property); + try { + if (p.isMultiple()) { + throw new UnsupportedOperationException("Multiple values properties are not supported"); + } + Value value = p.getValue(); + return (T) get(value); + } catch (ClassCastException e) { + throw new IllegalArgumentException( + "Cannot cast property of type " + PropertyType.nameFromValue(p.getType()), e); + } + } else { + return defaultValue; + } + } catch (RepositoryException e) { + throw new JcrException("Cannot retrieve property " + property + " from " + node, e); + } + } + + /** + * Get a multiple property as a list, doing a best effort to cast it as the + * target list. + * + * @return the value of {@link Node#getProperty(String)}. + * @throws IllegalArgumentException if the value could not be cast + * @throws JcrException in case of unexpected + * {@link RepositoryException} + */ + public static List getMultiple(Node node, String property) { + try { + if (node.hasProperty(property)) { + Property p = node.getProperty(property); + return getMultiple(p); + } else { + return null; + } + } catch (RepositoryException e) { + throw new JcrException("Cannot retrieve multiple values property " + property + " from " + node, e); + } + } + + /** + * Get a multiple property as a list, doing a best effort to cast it as the + * target list. + */ + @SuppressWarnings("unchecked") + public static List getMultiple(Property p) { + try { + List res = new ArrayList<>(); + if (!p.isMultiple()) { + res.add((T) get(p.getValue())); + return res; + } + Value[] values = p.getValues(); + for (Value value : values) { + res.add((T) get(value)); + } + return res; + } catch (ClassCastException | RepositoryException e) { + throw new IllegalArgumentException("Cannot get property " + p, e); + } + } + + /** Cast a {@link Value} to a standard Java object. */ + public static Object get(Value value) { + Binary binary = null; + try { + switch (value.getType()) { + case PropertyType.STRING: + return value.getString(); + case PropertyType.DOUBLE: + return (Double) value.getDouble(); + case PropertyType.LONG: + return (Long) value.getLong(); + case PropertyType.BOOLEAN: + return (Boolean) value.getBoolean(); + case PropertyType.DATE: + return value.getDate(); + case PropertyType.BINARY: + binary = value.getBinary(); + byte[] arr = null; + try (InputStream in = binary.getStream(); ByteArrayOutputStream out = new ByteArrayOutputStream();) { + IOUtils.copy(in, out); + arr = out.toByteArray(); + } catch (IOException e) { + throw new RuntimeException("Cannot read binary from " + value, e); + } + return arr; + default: + return value.getString(); + } + } catch (RepositoryException e) { + throw new JcrException("Cannot cast value from " + value, e); + } finally { + if (binary != null) + binary.dispose(); + } + } + + /** + * Retrieves the {@link Session} related to this node. + * + * @deprecated Use {@link #getSession(Node)} instead. + */ + @Deprecated + public static Session session(Node node) { + return getSession(node); + } + + /** Retrieves the {@link Session} related to this node. */ + public static Session getSession(Node node) { + try { + return node.getSession(); + } catch (RepositoryException e) { + throw new JcrException("Cannot retrieve session related to " + node, e); + } + } + + /** Retrieves the root node related to this session. */ + public static Node getRootNode(Session session) { + try { + return session.getRootNode(); + } catch (RepositoryException e) { + throw new JcrException("Cannot get root node for " + session, e); + } + } + + /** Whether this item exists. */ + public static boolean itemExists(Session session, String path) { + try { + return session.itemExists(path); + } catch (RepositoryException e) { + throw new JcrException("Cannot check whether " + path + " exists", e); + } + } + + /** + * Saves the {@link Session} related to this node. Note that all other unrelated + * modifications in this session will also be saved. + */ + public static void save(Node node) { + try { + Session session = node.getSession(); +// if (node.isNodeType(NodeType.MIX_LAST_MODIFIED)) { +// set(node, Property.JCR_LAST_MODIFIED, Instant.now()); +// set(node, Property.JCR_LAST_MODIFIED_BY, session.getUserID()); +// } + if (session.hasPendingChanges()) + session.save(); + } catch (RepositoryException e) { + throw new JcrException("Cannot save session related to " + node + " in workspace " + + session(node).getWorkspace().getName(), e); + } + } + + /** Login to a JCR repository. */ + public static Session login(Repository repository, String workspace) { + try { + return repository.login(workspace); + } catch (RepositoryException e) { + throw new IllegalArgumentException("Cannot login to repository", e); + } + } + + /** Safely and silently logs out a session. */ + public static void logout(Session session) { + try { + if (session != null) + if (session.isLive()) + session.logout(); + } catch (Exception e) { + // silent + } + } + + /** Safely and silently logs out the underlying session. */ + public static void logout(Node node) { + Jcr.logout(session(node)); + } + + /* + * SECURITY + */ + /** + * Add a single privilege to a node. + * + * @see Privilege + */ + public static void addPrivilege(Node node, String principal, String privilege) { + try { + Session session = node.getSession(); + JcrUtils.addPrivilege(session, node.getPath(), principal, privilege); + } catch (RepositoryException e) { + throw new JcrException("Cannot add privilege " + privilege + " to " + node, e); + } + } + + /* + * VERSIONING + */ + /** Get checked out status. */ + public static boolean isCheckedOut(Node node) { + try { + return node.isCheckedOut(); + } catch (RepositoryException e) { + throw new JcrException("Cannot retrieve checked out status of " + node, e); + } + } + + /** @see VersionManager#checkpoint(String) */ + public static void checkpoint(Node node) { + try { + versionManager(node).checkpoint(node.getPath()); + } catch (RepositoryException e) { + throw new JcrException("Cannot check in " + node, e); + } + } + + /** @see VersionManager#checkin(String) */ + public static void checkin(Node node) { + try { + versionManager(node).checkin(node.getPath()); + } catch (RepositoryException e) { + throw new JcrException("Cannot check in " + node, e); + } + } + + /** @see VersionManager#checkout(String) */ + public static void checkout(Node node) { + try { + versionManager(node).checkout(node.getPath()); + } catch (RepositoryException e) { + throw new JcrException("Cannot check out " + node, e); + } + } + + /** Get the {@link VersionManager} related to this node. */ + public static VersionManager versionManager(Node node) { + try { + return node.getSession().getWorkspace().getVersionManager(); + } catch (RepositoryException e) { + throw new JcrException("Cannot get version manager from " + node, e); + } + } + + /** Get the {@link VersionHistory} related to this node. */ + public static VersionHistory getVersionHistory(Node node) { + try { + return versionManager(node).getVersionHistory(node.getPath()); + } catch (RepositoryException e) { + throw new JcrException("Cannot get version history from " + node, e); + } + } + + /** + * The linear versions of this version history in reverse order and without the + * root version. + */ + public static List getLinearVersions(VersionHistory versionHistory) { + try { + List lst = new ArrayList<>(); + VersionIterator vit = versionHistory.getAllLinearVersions(); + while (vit.hasNext()) + lst.add(vit.nextVersion()); + lst.remove(0); + Collections.reverse(lst); + return lst; + } catch (RepositoryException e) { + throw new JcrException("Cannot get linear versions from " + versionHistory, e); + } + } + + /** The frozen node related to this {@link Version}. */ + public static Node getFrozenNode(Version version) { + try { + return version.getFrozenNode(); + } catch (RepositoryException e) { + throw new JcrException("Cannot get frozen node from " + version, e); + } + } + + /** Get the base {@link Version} related to this node. */ + public static Version getBaseVersion(Node node) { + try { + return versionManager(node).getBaseVersion(node.getPath()); + } catch (RepositoryException e) { + throw new JcrException("Cannot get base version from " + node, e); + } + } + + /* + * FILES + */ + /** + * Returns the size of this file. + * + * @see NodeType#NT_FILE + */ + public static long getFileSize(Node fileNode) { + try { + if (!fileNode.isNodeType(NodeType.NT_FILE)) + throw new IllegalArgumentException(fileNode + " must be a file."); + return getBinarySize(fileNode.getNode(Node.JCR_CONTENT).getProperty(Property.JCR_DATA).getBinary()); + } catch (RepositoryException e) { + throw new JcrException("Cannot get file size of " + fileNode, e); + } + } + + /** Returns the size of this {@link Binary}. */ + public static long getBinarySize(Binary binaryArg) { + try { + try (Bin binary = new Bin(binaryArg)) { + return binary.getSize(); + } + } catch (RepositoryException e) { + throw new JcrException("Cannot get file size of binary " + binaryArg, e); + } + } + + // QUERY + /** Creates a JCR-SQL2 query using {@link MessageFormat}. */ + public static Query createQuery(QueryManager qm, String sql, Object... args) { + // fix single quotes + sql = sql.replaceAll("'", "''"); + String query = MessageFormat.format(sql, args); + try { + return qm.createQuery(query, Query.JCR_SQL2); + } catch (RepositoryException e) { + throw new JcrException("Cannot create JCR-SQL2 query from " + query, e); + } + } + + /** Executes a JCR-SQL2 query using {@link MessageFormat}. */ + public static NodeIterator executeQuery(QueryManager qm, String sql, Object... args) { + Query query = createQuery(qm, sql, args); + try { + return query.execute().getNodes(); + } catch (RepositoryException e) { + throw new JcrException("Cannot execute query " + sql + " with arguments " + Arrays.asList(args), e); + } + } + + /** Executes a JCR-SQL2 query using {@link MessageFormat}. */ + public static NodeIterator executeQuery(Session session, String sql, Object... args) { + QueryManager queryManager; + try { + queryManager = session.getWorkspace().getQueryManager(); + } catch (RepositoryException e) { + throw new JcrException("Cannot get query manager from session " + session, e); + } + return executeQuery(queryManager, sql, args); + } + + /** + * Executes a JCR-SQL2 query using {@link MessageFormat}, which must return a + * single node at most. + * + * @return the node or null if not found. + */ + public static Node getNode(QueryManager qm, String sql, Object... args) { + NodeIterator nit = executeQuery(qm, sql, args); + if (nit.hasNext()) { + Node node = nit.nextNode(); + if (nit.hasNext()) + throw new IllegalStateException( + "Query " + sql + " with arguments " + Arrays.asList(args) + " returned more than one node."); + return node; + } else { + return null; + } + } + + /** + * Executes a JCR-SQL2 query using {@link MessageFormat}, which must return a + * single node at most. + * + * @return the node or null if not found. + */ + public static Node getNode(Session session, String sql, Object... args) { + QueryManager queryManager; + try { + queryManager = session.getWorkspace().getQueryManager(); + } catch (RepositoryException e) { + throw new JcrException("Cannot get query manager from session " + session, e); + } + return getNode(queryManager, sql, args); + } + + /** Singleton. */ + private Jcr() { + + } +} diff --git a/org.argeo.cms.jcr/src/org/argeo/jcr/JcrAuthorizations.java b/org.argeo.cms.jcr/src/org/argeo/jcr/JcrAuthorizations.java new file mode 100644 index 000000000..351929f8d --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/jcr/JcrAuthorizations.java @@ -0,0 +1,207 @@ +package org.argeo.jcr; + +import java.security.Principal; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.jcr.Repository; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.security.AccessControlManager; +import javax.jcr.security.Privilege; +import javax.naming.InvalidNameException; +import javax.naming.ldap.LdapName; + +/** Apply authorizations to a JCR repository. */ +public class JcrAuthorizations implements Runnable { + // private final static Log log = + // LogFactory.getLog(JcrAuthorizations.class); + + private Repository repository; + private String workspace = null; + + private String securityWorkspace = "security"; + + /** + * key := privilege1,privilege2/path/to/node
+ * value := group1,group2,user1 + */ + private Map principalPrivileges = new HashMap(); + + public void run() { + String currentWorkspace = workspace; + Session session = null; + try { + if (workspace != null && workspace.equals("*")) { + session = repository.login(); + String[] workspaces = session.getWorkspace().getAccessibleWorkspaceNames(); + JcrUtils.logoutQuietly(session); + for (String wksp : workspaces) { + currentWorkspace = wksp; + if (currentWorkspace.equals(securityWorkspace)) + continue; + session = repository.login(currentWorkspace); + initAuthorizations(session); + JcrUtils.logoutQuietly(session); + } + } else { + session = repository.login(workspace); + initAuthorizations(session); + } + } catch (RepositoryException e) { + JcrUtils.discardQuietly(session); + throw new JcrException( + "Cannot set authorizations " + principalPrivileges + " on workspace " + currentWorkspace, e); + } finally { + JcrUtils.logoutQuietly(session); + } + } + + protected void processWorkspace(String workspace) { + Session session = null; + try { + session = repository.login(workspace); + initAuthorizations(session); + } catch (RepositoryException e) { + JcrUtils.discardQuietly(session); + throw new JcrException( + "Cannot set authorizations " + principalPrivileges + " on repository " + repository, e); + } finally { + JcrUtils.logoutQuietly(session); + } + } + + /** @deprecated call {@link #run()} instead. */ + @Deprecated + public void init() { + run(); + } + + protected void initAuthorizations(Session session) throws RepositoryException { + AccessControlManager acm = session.getAccessControlManager(); + + for (String privileges : principalPrivileges.keySet()) { + String path = null; + int slashIndex = privileges.indexOf('/'); + if (slashIndex == 0) { + throw new IllegalArgumentException("Privilege " + privileges + " badly formatted it starts with /"); + } else if (slashIndex > 0) { + path = privileges.substring(slashIndex); + privileges = privileges.substring(0, slashIndex); + } + + if (path == null) + path = "/"; + + List privs = new ArrayList(); + for (String priv : privileges.split(",")) { + privs.add(acm.privilegeFromName(priv)); + } + + String principalNames = principalPrivileges.get(privileges); + try { + new LdapName(principalNames); + // TODO differentiate groups and users ? + Principal principal = getOrCreatePrincipal(session, principalNames); + JcrUtils.addPrivileges(session, path, principal, privs); + } catch (InvalidNameException e) { + for (String principalName : principalNames.split(",")) { + Principal principal = getOrCreatePrincipal(session, principalName); + JcrUtils.addPrivileges(session, path, principal, privs); + // if (log.isDebugEnabled()) { + // StringBuffer privBuf = new StringBuffer(); + // for (Privilege priv : privs) + // privBuf.append(priv.getName()); + // log.debug("Added privileges " + privBuf + " to " + // + principal.getName() + " on " + path + " in '" + // + session.getWorkspace().getName() + "'"); + // } + } + } + } + + // if (log.isDebugEnabled()) + // log.debug("JCR authorizations applied on '" + // + session.getWorkspace().getName() + "'"); + } + + /** + * Returns a {@link SimplePrincipal}, does not check whether it exists since + * such capabilities is not provided by the standard JCR API. Can be + * overridden to provide smarter handling + */ + protected Principal getOrCreatePrincipal(Session session, String principalName) throws RepositoryException { + return new SimplePrincipal(principalName); + } + + // public static void addPrivileges(Session session, Principal principal, + // String path, List privs) throws RepositoryException { + // AccessControlManager acm = session.getAccessControlManager(); + // // search for an access control list + // AccessControlList acl = null; + // AccessControlPolicyIterator policyIterator = acm + // .getApplicablePolicies(path); + // if (policyIterator.hasNext()) { + // while (policyIterator.hasNext()) { + // AccessControlPolicy acp = policyIterator + // .nextAccessControlPolicy(); + // if (acp instanceof AccessControlList) + // acl = ((AccessControlList) acp); + // } + // } else { + // AccessControlPolicy[] existingPolicies = acm.getPolicies(path); + // for (AccessControlPolicy acp : existingPolicies) { + // if (acp instanceof AccessControlList) + // acl = ((AccessControlList) acp); + // } + // } + // + // if (acl != null) { + // acl.addAccessControlEntry(principal, + // privs.toArray(new Privilege[privs.size()])); + // acm.setPolicy(path, acl); + // session.save(); + // if (log.isDebugEnabled()) { + // StringBuffer buf = new StringBuffer(""); + // for (int i = 0; i < privs.size(); i++) { + // if (i != 0) + // buf.append(','); + // buf.append(privs.get(i).getName()); + // } + // log.debug("Added privilege(s) '" + buf + "' to '" + // + principal.getName() + "' on " + path + // + " from workspace '" + // + session.getWorkspace().getName() + "'"); + // } + // } else { + // throw new ArgeoJcrException("Don't know how to apply privileges " + // + privs + " to " + principal + " on " + path + // + " from workspace '" + session.getWorkspace().getName() + // + "'"); + // } + // } + + @Deprecated + public void setGroupPrivileges(Map groupPrivileges) { + this.principalPrivileges = groupPrivileges; + } + + public void setPrincipalPrivileges(Map principalPrivileges) { + this.principalPrivileges = principalPrivileges; + } + + public void setRepository(Repository repository) { + this.repository = repository; + } + + public void setWorkspace(String workspace) { + this.workspace = workspace; + } + + public void setSecurityWorkspace(String securityWorkspace) { + this.securityWorkspace = securityWorkspace; + } + +} diff --git a/org.argeo.cms.jcr/src/org/argeo/jcr/JcrCallback.java b/org.argeo.cms.jcr/src/org/argeo/jcr/JcrCallback.java new file mode 100644 index 000000000..efbaabe82 --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/jcr/JcrCallback.java @@ -0,0 +1,15 @@ +package org.argeo.jcr; + +import java.util.function.Function; + +import javax.jcr.Session; + +/** An arbitrary execution on a JCR session, optionally returning a result. */ +@FunctionalInterface +public interface JcrCallback extends Function { + /** @deprecated Use {@link #apply(Session)} instead. */ + @Deprecated + public default Object execute(Session session) { + return apply(session); + } +} diff --git a/org.argeo.cms.jcr/src/org/argeo/jcr/JcrException.java b/org.argeo.cms.jcr/src/org/argeo/jcr/JcrException.java new file mode 100644 index 000000000..c77874376 --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/jcr/JcrException.java @@ -0,0 +1,22 @@ +package org.argeo.jcr; + +import javax.jcr.RepositoryException; + +/** + * Wraps a {@link RepositoryException} in a {@link RuntimeException}. + */ +public class JcrException extends IllegalStateException { + private static final long serialVersionUID = -4530350094877964989L; + + public JcrException(String message, RepositoryException e) { + super(message, e); + } + + public JcrException(RepositoryException e) { + super(e); + } + + public RepositoryException getRepositoryCause() { + return (RepositoryException) getCause(); + } +} diff --git a/org.argeo.cms.jcr/src/org/argeo/jcr/JcrMonitor.java b/org.argeo.cms.jcr/src/org/argeo/jcr/JcrMonitor.java new file mode 100644 index 000000000..71cf961e0 --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/jcr/JcrMonitor.java @@ -0,0 +1,87 @@ +package org.argeo.jcr; + + +/** + * Simple monitor abstraction. Inspired by Eclipse IProgressMOnitor, but without + * dependency to it. + */ +public interface JcrMonitor { + /** + * Constant indicating an unknown amount of work. + */ + public final static int UNKNOWN = -1; + + /** + * Notifies that the main task is beginning. This must only be called once + * on a given progress monitor instance. + * + * @param name + * the name (or description) of the main task + * @param totalWork + * the total number of work units into which the main task is + * been subdivided. If the value is UNKNOWN the + * implementation is free to indicate progress in a way which + * doesn't require the total number of work units in advance. + */ + public void beginTask(String name, int totalWork); + + /** + * Notifies that the work is done; that is, either the main task is + * completed or the user canceled it. This method may be called more than + * once (implementations should be prepared to handle this case). + */ + public void done(); + + /** + * Returns whether cancelation of current operation has been requested. + * Long-running operations should poll to see if cancelation has been + * requested. + * + * @return true if cancellation has been requested, and + * false otherwise + * @see #setCanceled(boolean) + */ + public boolean isCanceled(); + + /** + * Sets the cancel state to the given value. + * + * @param value + * true indicates that cancelation has been + * requested (but not necessarily acknowledged); + * false clears this flag + * @see #isCanceled() + */ + public void setCanceled(boolean value); + + /** + * Sets the task name to the given value. This method is used to restore the + * task label after a nested operation was executed. Normally there is no + * need for clients to call this method. + * + * @param name + * the name (or description) of the main task + * @see #beginTask(java.lang.String, int) + */ + public void setTaskName(String name); + + /** + * Notifies that a subtask of the main task is beginning. Subtasks are + * optional; the main task might not have subtasks. + * + * @param name + * the name (or description) of the subtask + */ + public void subTask(String name); + + /** + * Notifies that a given number of work unit of the main task has been + * completed. Note that this amount represents an installment, as opposed to + * a cumulative amount of work done to date. + * + * @param work + * a non-negative number of work units just completed + */ + public void worked(int work); + +} diff --git a/org.argeo.cms.jcr/src/org/argeo/jcr/JcrRepositoryWrapper.java b/org.argeo.cms.jcr/src/org/argeo/jcr/JcrRepositoryWrapper.java new file mode 100644 index 000000000..3228eee74 --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/jcr/JcrRepositoryWrapper.java @@ -0,0 +1,244 @@ +package org.argeo.jcr; + +import java.io.InputStream; +import java.math.BigDecimal; +import java.util.Arrays; +import java.util.Calendar; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.jcr.Binary; +import javax.jcr.Credentials; +import javax.jcr.LoginException; +import javax.jcr.NoSuchWorkspaceException; +import javax.jcr.PropertyType; +import javax.jcr.Repository; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.Value; +import javax.jcr.ValueFormatException; + +/** + * Wrapper around a JCR repository which allows to simplify configuration and + * intercept some actions. It exposes itself as a {@link Repository}. + */ +public abstract class JcrRepositoryWrapper implements Repository { + // private final static Log log = LogFactory + // .getLog(JcrRepositoryWrapper.class); + + // wrapped repository + private Repository repository; + + private Map additionalDescriptors = new HashMap<>(); + + private Boolean autocreateWorkspaces = false; + + public JcrRepositoryWrapper(Repository repository) { + setRepository(repository); + } + + /** + * Empty constructor + */ + public JcrRepositoryWrapper() { + } + + // /** Initializes */ + // public void init() { + // } + // + // /** Shutdown the repository */ + // public void destroy() throws Exception { + // } + + protected void putDescriptor(String key, String value) { + if (Arrays.asList(getRepository().getDescriptorKeys()).contains(key)) + throw new IllegalArgumentException("Descriptor key " + key + " is already defined in wrapped repository"); + if (value == null) + additionalDescriptors.remove(key); + else + additionalDescriptors.put(key, value); + } + + /* + * DELEGATED JCR REPOSITORY METHODS + */ + + public String getDescriptor(String key) { + if (additionalDescriptors.containsKey(key)) + return additionalDescriptors.get(key); + return getRepository().getDescriptor(key); + } + + public String[] getDescriptorKeys() { + if (additionalDescriptors.size() == 0) + return getRepository().getDescriptorKeys(); + List keys = Arrays.asList(getRepository().getDescriptorKeys()); + keys.addAll(additionalDescriptors.keySet()); + return keys.toArray(new String[keys.size()]); + } + + /** Central login method */ + public Session login(Credentials credentials, String workspaceName) + throws LoginException, NoSuchWorkspaceException, RepositoryException { + Session session; + try { + session = getRepository(workspaceName).login(credentials, workspaceName); + } catch (NoSuchWorkspaceException e) { + if (autocreateWorkspaces && workspaceName != null) + session = createWorkspaceAndLogsIn(credentials, workspaceName); + else + throw e; + } + processNewSession(session, workspaceName); + return session; + } + + public Session login() throws LoginException, RepositoryException { + return login(null, null); + } + + public Session login(Credentials credentials) throws LoginException, RepositoryException { + return login(credentials, null); + } + + public Session login(String workspaceName) throws LoginException, NoSuchWorkspaceException, RepositoryException { + return login(null, workspaceName); + } + + /** Called after a session has been created, does nothing by default. */ + protected void processNewSession(Session session, String workspaceName) { + } + + /** + * Wraps access to the repository, making sure it is available. + * + * @deprecated Use {@link #getDefaultRepository()} instead. + */ + @Deprecated + protected synchronized Repository getRepository() { + return getDefaultRepository(); + } + + protected synchronized Repository getDefaultRepository() { + return repository; + } + + protected synchronized Repository getRepository(String workspaceName) { + return getDefaultRepository(); + } + + /** + * Logs in to the default workspace, creates the required workspace, logs out, + * logs in to the required workspace. + */ + protected Session createWorkspaceAndLogsIn(Credentials credentials, String workspaceName) + throws RepositoryException { + if (workspaceName == null) + throw new IllegalArgumentException("No workspace specified."); + Session session = getRepository(workspaceName).login(credentials); + session.getWorkspace().createWorkspace(workspaceName); + session.logout(); + return getRepository(workspaceName).login(credentials, workspaceName); + } + + public boolean isStandardDescriptor(String key) { + return getRepository().isStandardDescriptor(key); + } + + public boolean isSingleValueDescriptor(String key) { + if (additionalDescriptors.containsKey(key)) + return true; + return getRepository().isSingleValueDescriptor(key); + } + + public Value getDescriptorValue(String key) { + if (additionalDescriptors.containsKey(key)) + return new StrValue(additionalDescriptors.get(key)); + return getRepository().getDescriptorValue(key); + } + + public Value[] getDescriptorValues(String key) { + return getRepository().getDescriptorValues(key); + } + + public synchronized void setRepository(Repository repository) { + this.repository = repository; + } + + public void setAutocreateWorkspaces(Boolean autocreateWorkspaces) { + this.autocreateWorkspaces = autocreateWorkspaces; + } + + protected static class StrValue implements Value { + private final String str; + + public StrValue(String str) { + this.str = str; + } + + @Override + public String getString() throws ValueFormatException, IllegalStateException, RepositoryException { + return str; + } + + @Override + public InputStream getStream() throws RepositoryException { + throw new UnsupportedOperationException(); + } + + @Override + public Binary getBinary() throws RepositoryException { + throw new UnsupportedOperationException(); + } + + @Override + public long getLong() throws ValueFormatException, RepositoryException { + try { + return Long.parseLong(str); + } catch (NumberFormatException e) { + throw new ValueFormatException("Cannot convert", e); + } + } + + @Override + public double getDouble() throws ValueFormatException, RepositoryException { + try { + return Double.parseDouble(str); + } catch (NumberFormatException e) { + throw new ValueFormatException("Cannot convert", e); + } + } + + @Override + public BigDecimal getDecimal() throws ValueFormatException, RepositoryException { + try { + return new BigDecimal(str); + } catch (NumberFormatException e) { + throw new ValueFormatException("Cannot convert", e); + } + } + + @Override + public Calendar getDate() throws ValueFormatException, RepositoryException { + throw new UnsupportedOperationException(); + } + + @Override + public boolean getBoolean() throws ValueFormatException, RepositoryException { + try { + return Boolean.parseBoolean(str); + } catch (NumberFormatException e) { + throw new ValueFormatException("Cannot convert", e); + } + } + + @Override + public int getType() { + return PropertyType.STRING; + } + + } + +} diff --git a/org.argeo.cms.jcr/src/org/argeo/jcr/JcrUrlStreamHandler.java b/org.argeo.cms.jcr/src/org/argeo/jcr/JcrUrlStreamHandler.java new file mode 100644 index 000000000..82a65e7f1 --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/jcr/JcrUrlStreamHandler.java @@ -0,0 +1,70 @@ +package org.argeo.jcr; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.net.URLConnection; +import java.net.URLStreamHandler; + +import javax.jcr.Item; +import javax.jcr.Node; +import javax.jcr.Property; +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.nodetype.NodeType; + +/** URL stream handler able to deal with nt:file node and properties. NOT FINISHED */ +public class JcrUrlStreamHandler extends URLStreamHandler { + private final Session session; + + public JcrUrlStreamHandler(Session session) { + this.session = session; + } + + @Override + protected URLConnection openConnection(final URL u) throws IOException { + // TODO Auto-generated method stub + return new URLConnection(u) { + + @Override + public void connect() throws IOException { + String itemPath = u.getPath(); + try { + if (!session.itemExists(itemPath)) + throw new IOException("No item under " + itemPath); + + Item item = session.getItem(u.getPath()); + if (item.isNode()) { + // this should be a nt:file node + Node node = (Node) item; + if (!node.getPrimaryNodeType().isNodeType( + NodeType.NT_FILE)) + throw new IOException("Node " + node + " is not a " + + NodeType.NT_FILE); + + } else { + Property property = (Property) item; + if(property.getType()==PropertyType.BINARY){ + //Binary binary = property.getBinary(); + + } + } + } catch (RepositoryException e) { + IOException ioe = new IOException( + "Unexpected JCR exception"); + ioe.initCause(e); + throw ioe; + } + } + + @Override + public InputStream getInputStream() throws IOException { + // TODO Auto-generated method stub + return super.getInputStream(); + } + + }; + } + +} diff --git a/org.argeo.cms.jcr/src/org/argeo/jcr/JcrUtils.java b/org.argeo.cms.jcr/src/org/argeo/jcr/JcrUtils.java new file mode 100644 index 000000000..3be8be184 --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/jcr/JcrUtils.java @@ -0,0 +1,1778 @@ +package org.argeo.jcr; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.PipedInputStream; +import java.io.PipedOutputStream; +import java.net.MalformedURLException; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.Principal; +import java.text.DateFormat; +import java.text.ParseException; +import java.time.Instant; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Collections; +import java.util.Date; +import java.util.GregorianCalendar; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; + +import javax.jcr.Binary; +import javax.jcr.Credentials; +import javax.jcr.ImportUUIDBehavior; +import javax.jcr.NamespaceRegistry; +import javax.jcr.NoSuchWorkspaceException; +import javax.jcr.Node; +import javax.jcr.NodeIterator; +import javax.jcr.Property; +import javax.jcr.PropertyIterator; +import javax.jcr.PropertyType; +import javax.jcr.Repository; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.Value; +import javax.jcr.Workspace; +import javax.jcr.nodetype.NoSuchNodeTypeException; +import javax.jcr.nodetype.NodeType; +import javax.jcr.observation.EventListener; +import javax.jcr.query.Query; +import javax.jcr.query.QueryResult; +import javax.jcr.security.AccessControlEntry; +import javax.jcr.security.AccessControlList; +import javax.jcr.security.AccessControlManager; +import javax.jcr.security.AccessControlPolicy; +import javax.jcr.security.AccessControlPolicyIterator; +import javax.jcr.security.Privilege; + +import org.apache.commons.io.IOUtils; + +/** Utility methods to simplify common JCR operations. */ +public class JcrUtils { + +// final private static Log log = LogFactory.getLog(JcrUtils.class); + + /** + * Not complete yet. See + * http://www.day.com/specs/jcr/2.0/3_Repository_Model.html#3.2.2%20Local + * %20Names + */ + public final static char[] INVALID_NAME_CHARACTERS = { '/', ':', '[', ']', '|', '*', /* invalid for XML: */ '<', + '>', '&' }; + + /** Prevents instantiation */ + private JcrUtils() { + } + + /** + * Queries one single node. + * + * @return one single node or null if none was found + * @throws JcrException if more than one node was found + */ + public static Node querySingleNode(Query query) { + NodeIterator nodeIterator; + try { + QueryResult queryResult = query.execute(); + nodeIterator = queryResult.getNodes(); + } catch (RepositoryException e) { + throw new JcrException("Cannot execute query " + query, e); + } + Node node; + if (nodeIterator.hasNext()) + node = nodeIterator.nextNode(); + else + return null; + + if (nodeIterator.hasNext()) + throw new IllegalArgumentException("Query returned more than one node."); + return node; + } + + /** Retrieves the node name from the provided path */ + public static String nodeNameFromPath(String path) { + if (path.equals("/")) + return ""; + if (path.charAt(0) != '/') + throw new IllegalArgumentException("Path " + path + " must start with a '/'"); + String pathT = path; + if (pathT.charAt(pathT.length() - 1) == '/') + pathT = pathT.substring(0, pathT.length() - 2); + + int index = pathT.lastIndexOf('/'); + return pathT.substring(index + 1); + } + + /** Retrieves the parent path of the provided path */ + public static String parentPath(String path) { + if (path.equals("/")) + throw new IllegalArgumentException("Root path '/' has no parent path"); + if (path.charAt(0) != '/') + throw new IllegalArgumentException("Path " + path + " must start with a '/'"); + String pathT = path; + if (pathT.charAt(pathT.length() - 1) == '/') + pathT = pathT.substring(0, pathT.length() - 2); + + int index = pathT.lastIndexOf('/'); + return pathT.substring(0, index); + } + + /** The provided data as a path ('/' at the end, not the beginning) */ + public static String dateAsPath(Calendar cal) { + return dateAsPath(cal, false); + } + + /** + * Creates a deep path based on a URL: + * http://subdomain.example.com/to/content?args becomes + * com/example/subdomain/to/content + */ + public static String urlAsPath(String url) { + try { + URL u = new URL(url); + StringBuffer path = new StringBuffer(url.length()); + // invert host + path.append(hostAsPath(u.getHost())); + // we don't put port since it may not always be there and may change + path.append(u.getPath()); + return path.toString(); + } catch (MalformedURLException e) { + throw new IllegalArgumentException("Cannot generate URL path for " + url, e); + } + } + + /** Set the {@link NodeType#NT_ADDRESS} properties based on this URL. */ + public static void urlToAddressProperties(Node node, String url) { + try { + URL u = new URL(url); + node.setProperty(Property.JCR_PROTOCOL, u.getProtocol()); + node.setProperty(Property.JCR_HOST, u.getHost()); + node.setProperty(Property.JCR_PORT, Integer.toString(u.getPort())); + node.setProperty(Property.JCR_PATH, normalizePath(u.getPath())); + } catch (RepositoryException e) { + throw new JcrException("Cannot set URL " + url + " as nt:address properties", e); + } catch (MalformedURLException e) { + throw new IllegalArgumentException("Cannot set URL " + url + " as nt:address properties", e); + } + } + + /** Build URL based on the {@link NodeType#NT_ADDRESS} properties. */ + public static String urlFromAddressProperties(Node node) { + try { + URL u = new URL(node.getProperty(Property.JCR_PROTOCOL).getString(), + node.getProperty(Property.JCR_HOST).getString(), + (int) node.getProperty(Property.JCR_PORT).getLong(), + node.getProperty(Property.JCR_PATH).getString()); + return u.toString(); + } catch (RepositoryException e) { + throw new JcrException("Cannot get URL from nt:address properties of " + node, e); + } catch (MalformedURLException e) { + throw new IllegalArgumentException("Cannot get URL from nt:address properties of " + node, e); + } + } + + /* + * PATH UTILITIES + */ + + /** + * Make sure that: starts with '/', do not end with '/', do not have '//' + */ + public static String normalizePath(String path) { + List tokens = tokenize(path); + StringBuffer buf = new StringBuffer(path.length()); + for (String token : tokens) { + buf.append('/'); + buf.append(token); + } + return buf.toString(); + } + + /** + * Creates a path from a FQDN, inverting the order of the component: + * www.argeo.org becomes org.argeo.www + */ + public static String hostAsPath(String host) { + StringBuffer path = new StringBuffer(host.length()); + String[] hostTokens = host.split("\\."); + for (int i = hostTokens.length - 1; i >= 0; i--) { + path.append(hostTokens[i]); + if (i != 0) + path.append('/'); + } + return path.toString(); + } + + /** + * Creates a path from a UUID (e.g. 6ebda899-217d-4bf1-abe4-2839085c8f3c becomes + * 6ebda899-217d/4bf1/abe4/2839085c8f3c/). '/' at the end, not the beginning + */ + public static String uuidAsPath(String uuid) { + StringBuffer path = new StringBuffer(uuid.length()); + String[] tokens = uuid.split("-"); + for (int i = 0; i < tokens.length; i++) { + path.append(tokens[i]); + if (i != 0) + path.append('/'); + } + return path.toString(); + } + + /** + * The provided data as a path ('/' at the end, not the beginning) + * + * @param cal the date + * @param addHour whether to add hour as well + */ + public static String dateAsPath(Calendar cal, Boolean addHour) { + StringBuffer buf = new StringBuffer(14); + buf.append('Y'); + buf.append(cal.get(Calendar.YEAR)); + buf.append('/'); + + int month = cal.get(Calendar.MONTH) + 1; + buf.append('M'); + if (month < 10) + buf.append(0); + buf.append(month); + buf.append('/'); + + int day = cal.get(Calendar.DAY_OF_MONTH); + buf.append('D'); + if (day < 10) + buf.append(0); + buf.append(day); + buf.append('/'); + + if (addHour) { + int hour = cal.get(Calendar.HOUR_OF_DAY); + buf.append('H'); + if (hour < 10) + buf.append(0); + buf.append(hour); + buf.append('/'); + } + return buf.toString(); + + } + + /** Converts in one call a string into a gregorian calendar. */ + public static Calendar parseCalendar(DateFormat dateFormat, String value) { + try { + Date date = dateFormat.parse(value); + Calendar calendar = new GregorianCalendar(); + calendar.setTime(date); + return calendar; + } catch (ParseException e) { + throw new IllegalArgumentException("Cannot parse " + value + " with date format " + dateFormat, e); + } + + } + + /** The last element of a path. */ + public static String lastPathElement(String path) { + if (path.charAt(path.length() - 1) == '/') + throw new IllegalArgumentException("Path " + path + " cannot end with '/'"); + int index = path.lastIndexOf('/'); + if (index < 0) + return path; + return path.substring(index + 1); + } + + /** + * Call {@link Node#getName()} without exceptions (useful in super + * constructors). + */ + public static String getNameQuietly(Node node) { + try { + return node.getName(); + } catch (RepositoryException e) { + throw new JcrException("Cannot get name from " + node, e); + } + } + + /** + * Call {@link Node#getProperty(String)} without exceptions (useful in super + * constructors). + */ + public static String getStringPropertyQuietly(Node node, String propertyName) { + try { + return node.getProperty(propertyName).getString(); + } catch (RepositoryException e) { + throw new JcrException("Cannot get name from " + node, e); + } + } + +// /** +// * Routine that get the child with this name, adding it if it does not already +// * exist +// */ +// public static Node getOrAdd(Node parent, String name, String primaryNodeType) throws RepositoryException { +// return parent.hasNode(name) ? parent.getNode(name) : parent.addNode(name, primaryNodeType); +// } + + /** + * Routine that get the child with this name, adding it if it does not already + * exist + */ + public static Node getOrAdd(Node parent, String name, String primaryNodeType, String... mixinNodeTypes) + throws RepositoryException { + Node node; + if (parent.hasNode(name)) { + node = parent.getNode(name); + if (primaryNodeType != null && !node.isNodeType(primaryNodeType)) + throw new IllegalArgumentException("Node " + node + " exists but is of primary node type " + + node.getPrimaryNodeType().getName() + ", not " + primaryNodeType); + for (String mixin : mixinNodeTypes) { + if (!node.isNodeType(mixin)) + node.addMixin(mixin); + } + return node; + } else { + node = primaryNodeType != null ? parent.addNode(name, primaryNodeType) : parent.addNode(name); + for (String mixin : mixinNodeTypes) { + node.addMixin(mixin); + } + return node; + } + } + + /** + * Routine that get the child with this name, adding it if it does not already + * exist + */ + public static Node getOrAdd(Node parent, String name) throws RepositoryException { + return parent.hasNode(name) ? parent.getNode(name) : parent.addNode(name); + } + + /** Convert a {@link NodeIterator} to a list of {@link Node} */ + public static List nodeIteratorToList(NodeIterator nodeIterator) { + List nodes = new ArrayList(); + while (nodeIterator.hasNext()) { + nodes.add(nodeIterator.nextNode()); + } + return nodes; + } + + /* + * PROPERTIES + */ + + /** + * Concisely get the string value of a property or null if this node doesn't + * have this property + */ + public static String get(Node node, String propertyName) { + try { + if (!node.hasProperty(propertyName)) + return null; + return node.getProperty(propertyName).getString(); + } catch (RepositoryException e) { + throw new JcrException("Cannot get property " + propertyName + " of " + node, e); + } + } + + /** Concisely get the path of the given node. */ + public static String getPath(Node node) { + try { + return node.getPath(); + } catch (RepositoryException e) { + throw new JcrException("Cannot get path of " + node, e); + } + } + + /** Concisely get the boolean value of a property */ + public static Boolean check(Node node, String propertyName) { + try { + return node.getProperty(propertyName).getBoolean(); + } catch (RepositoryException e) { + throw new JcrException("Cannot get property " + propertyName + " of " + node, e); + } + } + + /** Concisely get the bytes array value of a property */ + public static byte[] getBytes(Node node, String propertyName) { + try { + return getBinaryAsBytes(node.getProperty(propertyName)); + } catch (RepositoryException e) { + throw new JcrException("Cannot get property " + propertyName + " of " + node, e); + } + } + + /* + * MKDIRS + */ + + /** + * Create sub nodes relative to a parent node + */ + public static Node mkdirs(Node parentNode, String relativePath) { + return mkdirs(parentNode, relativePath, null, null); + } + + /** + * Create sub nodes relative to a parent node + * + * @param nodeType the type of the leaf node + */ + public static Node mkdirs(Node parentNode, String relativePath, String nodeType) { + return mkdirs(parentNode, relativePath, nodeType, null); + } + + /** + * Create sub nodes relative to a parent node + * + * @param nodeType the type of the leaf node + */ + public static Node mkdirs(Node parentNode, String relativePath, String nodeType, String intermediaryNodeType) { + List tokens = tokenize(relativePath); + Node currParent = parentNode; + try { + for (int i = 0; i < tokens.size(); i++) { + String name = tokens.get(i); + if (currParent.hasNode(name)) { + currParent = currParent.getNode(name); + } else { + if (i != (tokens.size() - 1)) {// intermediary + currParent = currParent.addNode(name, intermediaryNodeType); + } else {// leaf + currParent = currParent.addNode(name, nodeType); + } + } + } + return currParent; + } catch (RepositoryException e) { + throw new JcrException("Cannot mkdirs relative path " + relativePath + " from " + parentNode, e); + } + } + + /** + * Synchronized and save is performed, to avoid race conditions in initializers + * leading to duplicate nodes. + */ + public synchronized static Node mkdirsSafe(Session session, String path, String type) { + try { + if (session.hasPendingChanges()) + throw new IllegalStateException("Session has pending changes, save them first."); + Node node = mkdirs(session, path, type); + session.save(); + return node; + } catch (RepositoryException e) { + discardQuietly(session); + throw new JcrException("Cannot safely make directories", e); + } + } + + public synchronized static Node mkdirsSafe(Session session, String path) { + return mkdirsSafe(session, path, null); + } + + /** Creates the nodes making path, if they don't exist. */ + public static Node mkdirs(Session session, String path) { + return mkdirs(session, path, null, null, false); + } + + /** + * @param type the type of the leaf node + */ + public static Node mkdirs(Session session, String path, String type) { + return mkdirs(session, path, type, null, false); + } + + /** + * Creates the nodes making path, if they don't exist. This is up to the caller + * to save the session. Use with caution since it can create duplicate nodes if + * used concurrently. Requires read access to the root node of the workspace. + */ + public static Node mkdirs(Session session, String path, String type, String intermediaryNodeType, + Boolean versioning) { + try { + if (path.equals("/")) + return session.getRootNode(); + + if (session.itemExists(path)) { + Node node = session.getNode(path); + // check type + if (type != null && !node.isNodeType(type) && !node.getPath().equals("/")) + throw new IllegalArgumentException("Node " + node + " exists but is of type " + + node.getPrimaryNodeType().getName() + " not of type " + type); + // TODO: check versioning + return node; + } + + // StringBuffer current = new StringBuffer("/"); + // Node currentNode = session.getRootNode(); + + Node currentNode = findClosestExistingParent(session, path); + String closestExistingParentPath = currentNode.getPath(); + StringBuffer current = new StringBuffer(closestExistingParentPath); + if (!closestExistingParentPath.endsWith("/")) + current.append('/'); + Iterator it = tokenize(path.substring(closestExistingParentPath.length())).iterator(); + while (it.hasNext()) { + String part = it.next(); + current.append(part).append('/'); + if (!session.itemExists(current.toString())) { + if (!it.hasNext() && type != null) + currentNode = currentNode.addNode(part, type); + else if (it.hasNext() && intermediaryNodeType != null) + currentNode = currentNode.addNode(part, intermediaryNodeType); + else + currentNode = currentNode.addNode(part); + if (versioning) + currentNode.addMixin(NodeType.MIX_VERSIONABLE); +// if (log.isTraceEnabled()) +// log.debug("Added folder " + part + " as " + current); + } else { + currentNode = (Node) session.getItem(current.toString()); + } + } + return currentNode; + } catch (RepositoryException e) { + discardQuietly(session); + throw new JcrException("Cannot mkdirs " + path, e); + } finally { + } + } + + private static Node findClosestExistingParent(Session session, String path) throws RepositoryException { + int idx = path.lastIndexOf('/'); + if (idx == 0) + return session.getRootNode(); + String parentPath = path.substring(0, idx); + if (session.itemExists(parentPath)) + return session.getNode(parentPath); + else + return findClosestExistingParent(session, parentPath); + } + + /** Convert a path to the list of its tokens */ + public static List tokenize(String path) { + List tokens = new ArrayList(); + boolean optimized = false; + if (!optimized) { + String[] rawTokens = path.split("/"); + for (String token : rawTokens) { + if (!token.equals("")) + tokens.add(token); + } + } else { + StringBuffer curr = new StringBuffer(); + char[] arr = path.toCharArray(); + chars: for (int i = 0; i < arr.length; i++) { + char c = arr[i]; + if (c == '/') { + if (i == 0 || (i == arr.length - 1)) + continue chars; + if (curr.length() > 0) { + tokens.add(curr.toString()); + curr = new StringBuffer(); + } + } else + curr.append(c); + } + if (curr.length() > 0) { + tokens.add(curr.toString()); + curr = new StringBuffer(); + } + } + return Collections.unmodifiableList(tokens); + } + + // /** + // * use {@link #mkdirs(Session, String, String, String, Boolean)} instead. + // * + // * @deprecated + // */ + // @Deprecated + // public static Node mkdirs(Session session, String path, String type, + // Boolean versioning) { + // return mkdirs(session, path, type, type, false); + // } + + /** + * Safe and repository implementation independent registration of a namespace. + */ + public static void registerNamespaceSafely(Session session, String prefix, String uri) { + try { + registerNamespaceSafely(session.getWorkspace().getNamespaceRegistry(), prefix, uri); + } catch (RepositoryException e) { + throw new JcrException("Cannot find namespace registry", e); + } + } + + /** + * Safe and repository implementation independent registration of a namespace. + */ + public static void registerNamespaceSafely(NamespaceRegistry nr, String prefix, String uri) { + try { + String[] prefixes = nr.getPrefixes(); + for (String pref : prefixes) + if (pref.equals(prefix)) { + String registeredUri = nr.getURI(pref); + if (!registeredUri.equals(uri)) + throw new IllegalArgumentException("Prefix " + pref + " already registered for URI " + + registeredUri + " which is different from provided URI " + uri); + else + return;// skip + } + nr.registerNamespace(prefix, uri); + } catch (RepositoryException e) { + throw new JcrException("Cannot register namespace " + uri + " under prefix " + prefix, e); + } + } + +// /** Recursively outputs the contents of the given node. */ +// public static void debug(Node node) { +// debug(node, log); +// } +// +// /** Recursively outputs the contents of the given node. */ +// public static void debug(Node node, Log log) { +// try { +// // First output the node path +// log.debug(node.getPath()); +// // Skip the virtual (and large!) jcr:system subtree +// if (node.getName().equals("jcr:system")) { +// return; +// } +// +// // Then the children nodes (recursive) +// NodeIterator it = node.getNodes(); +// while (it.hasNext()) { +// Node childNode = it.nextNode(); +// debug(childNode, log); +// } +// +// // Then output the properties +// PropertyIterator properties = node.getProperties(); +// // log.debug("Property are : "); +// +// properties: while (properties.hasNext()) { +// Property property = properties.nextProperty(); +// if (property.getType() == PropertyType.BINARY) +// continue properties;// skip +// if (property.getDefinition().isMultiple()) { +// // A multi-valued property, print all values +// Value[] values = property.getValues(); +// for (int i = 0; i < values.length; i++) { +// log.debug(property.getPath() + "=" + values[i].getString()); +// } +// } else { +// // A single-valued property +// log.debug(property.getPath() + "=" + property.getString()); +// } +// } +// } catch (Exception e) { +// log.error("Could not debug " + node, e); +// } +// +// } + +// /** Logs the effective access control policies */ +// public static void logEffectiveAccessPolicies(Node node) { +// try { +// logEffectiveAccessPolicies(node.getSession(), node.getPath()); +// } catch (RepositoryException e) { +// log.error("Cannot log effective access policies of " + node, e); +// } +// } +// +// /** Logs the effective access control policies */ +// public static void logEffectiveAccessPolicies(Session session, String path) { +// if (!log.isDebugEnabled()) +// return; +// +// try { +// AccessControlPolicy[] effectivePolicies = session.getAccessControlManager().getEffectivePolicies(path); +// if (effectivePolicies.length > 0) { +// for (AccessControlPolicy policy : effectivePolicies) { +// if (policy instanceof AccessControlList) { +// AccessControlList acl = (AccessControlList) policy; +// log.debug("Access control list for " + path + "\n" + accessControlListSummary(acl)); +// } +// } +// } else { +// log.debug("No effective access control policy for " + path); +// } +// } catch (RepositoryException e) { +// log.error("Cannot log effective access policies of " + path, e); +// } +// } + + /** Returns a human-readable summary of this access control list. */ + public static String accessControlListSummary(AccessControlList acl) { + StringBuffer buf = new StringBuffer(""); + try { + for (AccessControlEntry ace : acl.getAccessControlEntries()) { + buf.append('\t').append(ace.getPrincipal().getName()).append('\n'); + for (Privilege priv : ace.getPrivileges()) + buf.append("\t\t").append(priv.getName()).append('\n'); + } + return buf.toString(); + } catch (RepositoryException e) { + throw new JcrException("Cannot write summary of " + acl, e); + } + } + + /** Copy the whole workspace via a system view XML. */ + public static void copyWorkspaceXml(Session fromSession, Session toSession) { + Workspace fromWorkspace = fromSession.getWorkspace(); + Workspace toWorkspace = toSession.getWorkspace(); + String errorMsg = "Cannot copy workspace " + fromWorkspace + " to " + toWorkspace + " via XML."; + + try (PipedInputStream in = new PipedInputStream(1024 * 1024);) { + new Thread(() -> { + try (PipedOutputStream out = new PipedOutputStream(in)) { + fromSession.exportSystemView("/", out, false, false); + out.flush(); + } catch (IOException e) { + throw new RuntimeException(errorMsg, e); + } catch (RepositoryException e) { + throw new JcrException(errorMsg, e); + } + }, "Copy workspace" + fromWorkspace + " to " + toWorkspace).start(); + + toSession.importXML("/", in, ImportUUIDBehavior.IMPORT_UUID_COLLISION_REPLACE_EXISTING); + toSession.save(); + } catch (IOException e) { + throw new RuntimeException(errorMsg, e); + } catch (RepositoryException e) { + throw new JcrException(errorMsg, e); + } + } + + /** + * Copies recursively the content of a node to another one. Do NOT copy the + * property values of {@link NodeType#MIX_CREATED} and + * {@link NodeType#MIX_LAST_MODIFIED}, but update the + * {@link Property#JCR_LAST_MODIFIED} and {@link Property#JCR_LAST_MODIFIED_BY} + * properties if the target node has the {@link NodeType#MIX_LAST_MODIFIED} + * mixin. + */ + public static void copy(Node fromNode, Node toNode) { + try { + if (toNode.getDefinition().isProtected()) + return; + + // add mixins + for (NodeType mixinType : fromNode.getMixinNodeTypes()) { + try { + toNode.addMixin(mixinType.getName()); + } catch (NoSuchNodeTypeException e) { + // ignore unknown mixins + // TODO log it + } + } + + // process properties + PropertyIterator pit = fromNode.getProperties(); + properties: while (pit.hasNext()) { + Property fromProperty = pit.nextProperty(); + String propertyName = fromProperty.getName(); + if (toNode.hasProperty(propertyName) && toNode.getProperty(propertyName).getDefinition().isProtected()) + continue properties; + + if (fromProperty.getDefinition().isProtected()) + continue properties; + + if (propertyName.equals("jcr:created") || propertyName.equals("jcr:createdBy") + || propertyName.equals("jcr:lastModified") || propertyName.equals("jcr:lastModifiedBy")) + continue properties; + + if (fromProperty.isMultiple()) { + toNode.setProperty(propertyName, fromProperty.getValues()); + } else { + toNode.setProperty(propertyName, fromProperty.getValue()); + } + } + + // update jcr:lastModified and jcr:lastModifiedBy in toNode in case + // they existed, before adding the mixins + updateLastModified(toNode, true); + + // process children nodes + NodeIterator nit = fromNode.getNodes(); + while (nit.hasNext()) { + Node fromChild = nit.nextNode(); + Integer index = fromChild.getIndex(); + String nodeRelPath = fromChild.getName() + "[" + index + "]"; + Node toChild; + if (toNode.hasNode(nodeRelPath)) + toChild = toNode.getNode(nodeRelPath); + else { + try { + toChild = toNode.addNode(fromChild.getName(), fromChild.getPrimaryNodeType().getName()); + } catch (NoSuchNodeTypeException e) { + // ignore unknown primary types + // TODO log it + return; + } + } + copy(fromChild, toChild); + } + } catch (RepositoryException e) { + throw new JcrException("Cannot copy " + fromNode + " to " + toNode, e); + } + } + + /** + * Check whether all first-level properties (except jcr:* properties) are equal. + * Skip jcr:* properties + */ + public static Boolean allPropertiesEquals(Node reference, Node observed, Boolean onlyCommonProperties) { + try { + PropertyIterator pit = reference.getProperties(); + props: while (pit.hasNext()) { + Property propReference = pit.nextProperty(); + String propName = propReference.getName(); + if (propName.startsWith("jcr:")) + continue props; + + if (!observed.hasProperty(propName)) + if (onlyCommonProperties) + continue props; + else + return false; + // TODO: deal with multiple property values? + if (!observed.getProperty(propName).getValue().equals(propReference.getValue())) + return false; + } + return true; + } catch (RepositoryException e) { + throw new JcrException("Cannot check all properties equals of " + reference + " and " + observed, e); + } + } + + public static Map diffProperties(Node reference, Node observed) { + Map diffs = new TreeMap(); + diffPropertiesLevel(diffs, null, reference, observed); + return diffs; + } + + /** + * Compare the properties of two nodes. Recursivity to child nodes is not yet + * supported. Skip jcr:* properties. + */ + static void diffPropertiesLevel(Map diffs, String baseRelPath, Node reference, + Node observed) { + try { + // check removed and modified + PropertyIterator pit = reference.getProperties(); + props: while (pit.hasNext()) { + Property p = pit.nextProperty(); + String name = p.getName(); + if (name.startsWith("jcr:")) + continue props; + + if (!observed.hasProperty(name)) { + String relPath = propertyRelPath(baseRelPath, name); + PropertyDiff pDiff = new PropertyDiff(PropertyDiff.REMOVED, relPath, p.getValue(), null); + diffs.put(relPath, pDiff); + } else { + if (p.isMultiple()) { + // FIXME implement multiple + } else { + Value referenceValue = p.getValue(); + Value newValue = observed.getProperty(name).getValue(); + if (!referenceValue.equals(newValue)) { + String relPath = propertyRelPath(baseRelPath, name); + PropertyDiff pDiff = new PropertyDiff(PropertyDiff.MODIFIED, relPath, referenceValue, + newValue); + diffs.put(relPath, pDiff); + } + } + } + } + // check added + pit = observed.getProperties(); + props: while (pit.hasNext()) { + Property p = pit.nextProperty(); + String name = p.getName(); + if (name.startsWith("jcr:")) + continue props; + if (!reference.hasProperty(name)) { + if (p.isMultiple()) { + // FIXME implement multiple + } else { + String relPath = propertyRelPath(baseRelPath, name); + PropertyDiff pDiff = new PropertyDiff(PropertyDiff.ADDED, relPath, null, p.getValue()); + diffs.put(relPath, pDiff); + } + } + } + } catch (RepositoryException e) { + throw new JcrException("Cannot diff " + reference + " and " + observed, e); + } + } + + /** + * Compare only a restricted list of properties of two nodes. No recursivity. + * + */ + public static Map diffProperties(Node reference, Node observed, List properties) { + Map diffs = new TreeMap(); + try { + Iterator pit = properties.iterator(); + + props: while (pit.hasNext()) { + String name = pit.next(); + if (!reference.hasProperty(name)) { + if (!observed.hasProperty(name)) + continue props; + Value val = observed.getProperty(name).getValue(); + try { + // empty String but not null + if ("".equals(val.getString())) + continue props; + } catch (Exception e) { + // not parseable as String, silent + } + PropertyDiff pDiff = new PropertyDiff(PropertyDiff.ADDED, name, null, val); + diffs.put(name, pDiff); + } else if (!observed.hasProperty(name)) { + PropertyDiff pDiff = new PropertyDiff(PropertyDiff.REMOVED, name, + reference.getProperty(name).getValue(), null); + diffs.put(name, pDiff); + } else { + Value referenceValue = reference.getProperty(name).getValue(); + Value newValue = observed.getProperty(name).getValue(); + if (!referenceValue.equals(newValue)) { + PropertyDiff pDiff = new PropertyDiff(PropertyDiff.MODIFIED, name, referenceValue, newValue); + diffs.put(name, pDiff); + } + } + } + } catch (RepositoryException e) { + throw new JcrException("Cannot diff " + reference + " and " + observed, e); + } + return diffs; + } + + /** Builds a property relPath to be used in the diff. */ + private static String propertyRelPath(String baseRelPath, String propertyName) { + if (baseRelPath == null) + return propertyName; + else + return baseRelPath + '/' + propertyName; + } + + /** + * Normalizes a name so that it can be stored in contexts not supporting names + * with ':' (typically databases). Replaces ':' by '_'. + */ + public static String normalize(String name) { + return name.replace(':', '_'); + } + + /** + * Replaces characters which are invalid in a JCR name by '_'. Currently not + * exhaustive. + * + * @see JcrUtils#INVALID_NAME_CHARACTERS + */ + public static String replaceInvalidChars(String name) { + return replaceInvalidChars(name, '_'); + } + + /** + * Replaces characters which are invalid in a JCR name. Currently not + * exhaustive. + * + * @see JcrUtils#INVALID_NAME_CHARACTERS + */ + public static String replaceInvalidChars(String name, char replacement) { + boolean modified = false; + char[] arr = name.toCharArray(); + for (int i = 0; i < arr.length; i++) { + char c = arr[i]; + invalid: for (char invalid : INVALID_NAME_CHARACTERS) { + if (c == invalid) { + arr[i] = replacement; + modified = true; + break invalid; + } + } + } + if (modified) + return new String(arr); + else + // do not create new object if unnecessary + return name; + } + + // /** + // * Removes forbidden characters from a path, replacing them with '_' + // * + // * @deprecated use {@link #replaceInvalidChars(String)} instead + // */ + // public static String removeForbiddenCharacters(String str) { + // return str.replace('[', '_').replace(']', '_').replace('/', '_').replace('*', + // '_'); + // + // } + + /** Cleanly disposes a {@link Binary} even if it is null. */ + public static void closeQuietly(Binary binary) { + if (binary == null) + return; + binary.dispose(); + } + + /** Retrieve a {@link Binary} as a byte array */ + public static byte[] getBinaryAsBytes(Property property) { + try (ByteArrayOutputStream out = new ByteArrayOutputStream(); + Bin binary = new Bin(property); + InputStream in = binary.getStream()) { + IOUtils.copy(in, out); + return out.toByteArray(); + } catch (RepositoryException e) { + throw new JcrException("Cannot read binary " + property + " as bytes", e); + } catch (IOException e) { + throw new RuntimeException("Cannot read binary " + property + " as bytes", e); + } + } + + /** Writes a {@link Binary} from a byte array */ + public static void setBinaryAsBytes(Node node, String property, byte[] bytes) { + Binary binary = null; + try (InputStream in = new ByteArrayInputStream(bytes)) { + binary = node.getSession().getValueFactory().createBinary(in); + node.setProperty(property, binary); + } catch (RepositoryException e) { + throw new JcrException("Cannot set binary " + property + " as bytes", e); + } catch (IOException e) { + throw new RuntimeException("Cannot set binary " + property + " as bytes", e); + } finally { + closeQuietly(binary); + } + } + + /** Writes a {@link Binary} from a byte array */ + public static void setBinaryAsBytes(Property prop, byte[] bytes) { + Binary binary = null; + try (InputStream in = new ByteArrayInputStream(bytes)) { + binary = prop.getSession().getValueFactory().createBinary(in); + prop.setValue(binary); + } catch (RepositoryException e) { + throw new JcrException("Cannot set binary " + prop + " as bytes", e); + } catch (IOException e) { + throw new RuntimeException("Cannot set binary " + prop + " as bytes", e); + } finally { + closeQuietly(binary); + } + } + + /** + * Creates depth from a string (typically a username) by adding levels based on + * its first characters: "aBcD",2 becomes a/aB + */ + public static String firstCharsToPath(String str, Integer nbrOfChars) { + if (str.length() < nbrOfChars) + throw new IllegalArgumentException("String " + str + " length must be greater or equal than " + nbrOfChars); + StringBuffer path = new StringBuffer(""); + StringBuffer curr = new StringBuffer(""); + for (int i = 0; i < nbrOfChars; i++) { + curr.append(str.charAt(i)); + path.append(curr); + if (i < nbrOfChars - 1) + path.append('/'); + } + return path.toString(); + } + + /** + * Discards the current changes in the session attached to this node. To be used + * typically in a catch block. + * + * @see #discardQuietly(Session) + */ + public static void discardUnderlyingSessionQuietly(Node node) { + try { + discardQuietly(node.getSession()); + } catch (RepositoryException e) { + // silent + } + } + + /** + * Discards the current changes in a session by calling + * {@link Session#refresh(boolean)} with false, only logging + * potential errors when doing so. To be used typically in a catch block. + */ + public static void discardQuietly(Session session) { + try { + if (session != null) + session.refresh(false); + } catch (RepositoryException e) { + // silent + } + } + + /** + * Login to a workspace with implicit credentials, creates the workspace with + * these credentials if it does not already exist. + */ + public static Session loginOrCreateWorkspace(Repository repository, String workspaceName) + throws RepositoryException { + return loginOrCreateWorkspace(repository, workspaceName, null); + } + + /** + * Login to a workspace with implicit credentials, creates the workspace with + * these credentials if it does not already exist. + */ + public static Session loginOrCreateWorkspace(Repository repository, String workspaceName, Credentials credentials) + throws RepositoryException { + Session workspaceSession = null; + Session defaultSession = null; + try { + try { + workspaceSession = repository.login(credentials, workspaceName); + } catch (NoSuchWorkspaceException e) { + // try to create workspace + defaultSession = repository.login(credentials); + defaultSession.getWorkspace().createWorkspace(workspaceName); + workspaceSession = repository.login(credentials, workspaceName); + } + return workspaceSession; + } finally { + logoutQuietly(defaultSession); + } + } + + /** + * Logs out the session, not throwing any exception, even if it is null. + * {@link Jcr#logout(Session)} should rather be used. + */ + public static void logoutQuietly(Session session) { + Jcr.logout(session); +// try { +// if (session != null) +// if (session.isLive()) +// session.logout(); +// } catch (Exception e) { +// // silent +// } + } + + /** + * Convenient method to add a listener. uuids passed as null, deep=true, + * local=true, only one node type + */ + public static void addListener(Session session, EventListener listener, int eventTypes, String basePath, + String nodeType) { + try { + session.getWorkspace().getObservationManager().addEventListener(listener, eventTypes, basePath, true, null, + nodeType == null ? null : new String[] { nodeType }, true); + } catch (RepositoryException e) { + throw new JcrException("Cannot add JCR listener " + listener + " to session " + session, e); + } + } + + /** Removes a listener without throwing exception */ + public static void removeListenerQuietly(Session session, EventListener listener) { + if (session == null || !session.isLive()) + return; + try { + session.getWorkspace().getObservationManager().removeEventListener(listener); + } catch (RepositoryException e) { + // silent + } + } + + /** + * Quietly unregisters an {@link EventListener} from the udnerlying workspace of + * this node. + */ + public static void unregisterQuietly(Node node, EventListener eventListener) { + try { + unregisterQuietly(node.getSession().getWorkspace(), eventListener); + } catch (RepositoryException e) { + // silent + } + } + + /** Quietly unregisters an {@link EventListener} from this workspace */ + public static void unregisterQuietly(Workspace workspace, EventListener eventListener) { + if (eventListener == null) + return; + try { + workspace.getObservationManager().removeEventListener(eventListener); + } catch (RepositoryException e) { + // silent + } + } + + /** + * Checks whether {@link Property#JCR_LAST_MODIFIED} or (afterwards) + * {@link Property#JCR_CREATED} are set and returns it as an {@link Instant}. + */ + public static Instant getModified(Node node) { + Calendar calendar = null; + try { + if (node.hasProperty(Property.JCR_LAST_MODIFIED)) + calendar = node.getProperty(Property.JCR_LAST_MODIFIED).getDate(); + else if (node.hasProperty(Property.JCR_CREATED)) + calendar = node.getProperty(Property.JCR_CREATED).getDate(); + else + throw new IllegalArgumentException("No modification time found in " + node); + return calendar.toInstant(); + } catch (RepositoryException e) { + throw new JcrException("Cannot get modification time for " + node, e); + } + + } + + /** + * Get {@link Property#JCR_CREATED} as an {@link Instant}, if it is set. + */ + public static Instant getCreated(Node node) { + Calendar calendar = null; + try { + if (node.hasProperty(Property.JCR_CREATED)) + calendar = node.getProperty(Property.JCR_CREATED).getDate(); + else + throw new IllegalArgumentException("No created time found in " + node); + return calendar.toInstant(); + } catch (RepositoryException e) { + throw new JcrException("Cannot get created time for " + node, e); + } + + } + + /** + * Updates the {@link Property#JCR_LAST_MODIFIED} property with the current time + * and the {@link Property#JCR_LAST_MODIFIED_BY} property with the underlying + * session user id. + */ + public static void updateLastModified(Node node) { + updateLastModified(node, false); + } + + /** + * Updates the {@link Property#JCR_LAST_MODIFIED} property with the current time + * and the {@link Property#JCR_LAST_MODIFIED_BY} property with the underlying + * session user id. In Jackrabbit 2.x, + * these properties are + * not automatically updated, hence the need for manual update. The session + * is not saved. + */ + public static void updateLastModified(Node node, boolean addMixin) { + try { + if (addMixin && !node.isNodeType(NodeType.MIX_LAST_MODIFIED)) + node.addMixin(NodeType.MIX_LAST_MODIFIED); + node.setProperty(Property.JCR_LAST_MODIFIED, new GregorianCalendar()); + node.setProperty(Property.JCR_LAST_MODIFIED_BY, node.getSession().getUserID()); + } catch (RepositoryException e) { + throw new JcrException("Cannot update last modified on " + node, e); + } + } + + /** + * Update lastModified recursively until this parent. + * + * @param node the node + * @param untilPath the base path, null is equivalent to "/" + */ + public static void updateLastModifiedAndParents(Node node, String untilPath) { + updateLastModifiedAndParents(node, untilPath, true); + } + + /** + * Update lastModified recursively until this parent. + * + * @param node the node + * @param untilPath the base path, null is equivalent to "/" + */ + public static void updateLastModifiedAndParents(Node node, String untilPath, boolean addMixin) { + try { + if (untilPath != null && !node.getPath().startsWith(untilPath)) + throw new IllegalArgumentException(node + " is not under " + untilPath); + updateLastModified(node, addMixin); + if (untilPath == null) { + if (!node.getPath().equals("/")) + updateLastModifiedAndParents(node.getParent(), untilPath, addMixin); + } else { + if (!node.getPath().equals(untilPath)) + updateLastModifiedAndParents(node.getParent(), untilPath, addMixin); + } + } catch (RepositoryException e) { + throw new JcrException("Cannot update lastModified from " + node + " until " + untilPath, e); + } + } + + /** + * Returns a String representing the short version (see + * Node type + * Notation attributes grammar) of the main business attributes of this + * property definition + * + * @param prop + */ + public static String getPropertyDefinitionAsString(Property prop) { + StringBuffer sbuf = new StringBuffer(); + try { + if (prop.getDefinition().isAutoCreated()) + sbuf.append("a"); + if (prop.getDefinition().isMandatory()) + sbuf.append("m"); + if (prop.getDefinition().isProtected()) + sbuf.append("p"); + if (prop.getDefinition().isMultiple()) + sbuf.append("*"); + } catch (RepositoryException re) { + throw new JcrException("unexpected error while getting property definition as String", re); + } + return sbuf.toString(); + } + + /** + * Estimate the sub tree size from current node. Computation is based on the Jcr + * {@link Property#getLength()} method. Note : it is not the exact size used on + * the disk by the current part of the JCR Tree. + */ + + public static long getNodeApproxSize(Node node) { + long curNodeSize = 0; + try { + PropertyIterator pi = node.getProperties(); + while (pi.hasNext()) { + Property prop = pi.nextProperty(); + if (prop.isMultiple()) { + int nb = prop.getLengths().length; + for (int i = 0; i < nb; i++) { + curNodeSize += (prop.getLengths()[i] > 0 ? prop.getLengths()[i] : 0); + } + } else + curNodeSize += (prop.getLength() > 0 ? prop.getLength() : 0); + } + + NodeIterator ni = node.getNodes(); + while (ni.hasNext()) + curNodeSize += getNodeApproxSize(ni.nextNode()); + return curNodeSize; + } catch (RepositoryException re) { + throw new JcrException("Unexpected error while recursively determining node size.", re); + } + } + + /* + * SECURITY + */ + + /** + * Convenience method for adding a single privilege to a principal (user or + * role), typically jcr:all + */ + public synchronized static void addPrivilege(Session session, String path, String principal, String privilege) + throws RepositoryException { + List privileges = new ArrayList(); + privileges.add(session.getAccessControlManager().privilegeFromName(privilege)); + addPrivileges(session, path, new SimplePrincipal(principal), privileges); + } + + /** + * Add privileges on a path to a {@link Principal}. The path must already exist. + * Session is saved. Synchronized to prevent concurrent modifications of the + * same node. + */ + public synchronized static Boolean addPrivileges(Session session, String path, Principal principal, + List privs) throws RepositoryException { + // make sure the session is in line with the persisted state + session.refresh(false); + AccessControlManager acm = session.getAccessControlManager(); + AccessControlList acl = getAccessControlList(acm, path); + + accessControlEntries: for (AccessControlEntry ace : acl.getAccessControlEntries()) { + Principal currentPrincipal = ace.getPrincipal(); + if (currentPrincipal.getName().equals(principal.getName())) { + Privilege[] currentPrivileges = ace.getPrivileges(); + if (currentPrivileges.length != privs.size()) + break accessControlEntries; + for (int i = 0; i < currentPrivileges.length; i++) { + Privilege currP = currentPrivileges[i]; + Privilege p = privs.get(i); + if (!currP.getName().equals(p.getName())) { + break accessControlEntries; + } + } + return false; + } + } + + Privilege[] privileges = privs.toArray(new Privilege[privs.size()]); + acl.addAccessControlEntry(principal, privileges); + acm.setPolicy(path, acl); +// if (log.isDebugEnabled()) { +// StringBuffer privBuf = new StringBuffer(); +// for (Privilege priv : privs) +// privBuf.append(priv.getName()); +// log.debug("Added privileges " + privBuf + " to " + principal.getName() + " on " + path + " in '" +// + session.getWorkspace().getName() + "'"); +// } + session.refresh(true); + session.save(); + return true; + } + + /** + * Gets the first available access control list for this path, throws exception + * if not found + */ + public synchronized static AccessControlList getAccessControlList(AccessControlManager acm, String path) + throws RepositoryException { + // search for an access control list + AccessControlList acl = null; + AccessControlPolicyIterator policyIterator = acm.getApplicablePolicies(path); + applicablePolicies: if (policyIterator.hasNext()) { + while (policyIterator.hasNext()) { + AccessControlPolicy acp = policyIterator.nextAccessControlPolicy(); + if (acp instanceof AccessControlList) { + acl = ((AccessControlList) acp); + break applicablePolicies; + } + } + } else { + AccessControlPolicy[] existingPolicies = acm.getPolicies(path); + existingPolicies: for (AccessControlPolicy acp : existingPolicies) { + if (acp instanceof AccessControlList) { + acl = ((AccessControlList) acp); + break existingPolicies; + } + } + } + if (acl != null) + return acl; + else + throw new IllegalArgumentException("ACL not found at " + path); + } + + /** Clear authorizations for a user at this path */ + public synchronized static void clearAccessControList(Session session, String path, String username) + throws RepositoryException { + AccessControlManager acm = session.getAccessControlManager(); + AccessControlList acl = getAccessControlList(acm, path); + for (AccessControlEntry ace : acl.getAccessControlEntries()) { + if (ace.getPrincipal().getName().equals(username)) { + acl.removeAccessControlEntry(ace); + } + } + // the new access control list must be applied otherwise this call: + // acl.removeAccessControlEntry(ace); has no effect + acm.setPolicy(path, acl); + session.refresh(true); + session.save(); + } + + /* + * FILES UTILITIES + */ + /** + * Creates the nodes making the path as {@link NodeType#NT_FOLDER} + */ + public static Node mkfolders(Session session, String path) { + return mkdirs(session, path, NodeType.NT_FOLDER, NodeType.NT_FOLDER, false); + } + + /** + * Copy only nt:folder and nt:file, without their additional types and + * properties. + * + * @param recursive if true copies folders as well, otherwise only first level + * files + * @return how many files were copied + */ + public static Long copyFiles(Node fromNode, Node toNode, Boolean recursive, JcrMonitor monitor, boolean onlyAdd) { + long count = 0l; + + // Binary binary = null; + // InputStream in = null; + try { + NodeIterator fromChildren = fromNode.getNodes(); + children: while (fromChildren.hasNext()) { + if (monitor != null && monitor.isCanceled()) + throw new IllegalStateException("Copy cancelled before it was completed"); + + Node fromChild = fromChildren.nextNode(); + String fileName = fromChild.getName(); + if (fromChild.isNodeType(NodeType.NT_FILE)) { + if (onlyAdd && toNode.hasNode(fileName)) { + monitor.subTask("Skip existing " + fileName); + continue children; + } + + if (monitor != null) + monitor.subTask("Copy " + fileName); + try (Bin binary = new Bin(fromChild.getNode(Node.JCR_CONTENT).getProperty(Property.JCR_DATA)); + InputStream in = binary.getStream();) { + copyStreamAsFile(toNode, fileName, in); + } catch (IOException e) { + throw new RuntimeException("Cannot copy " + fileName + " to " + toNode, e); + } + + // save session + toNode.getSession().save(); + count++; + +// if (log.isDebugEnabled()) +// log.debug("Copied file " + fromChild.getPath()); + if (monitor != null) + monitor.worked(1); + } else if (fromChild.isNodeType(NodeType.NT_FOLDER) && recursive) { + Node toChildFolder; + if (toNode.hasNode(fileName)) { + toChildFolder = toNode.getNode(fileName); + if (!toChildFolder.isNodeType(NodeType.NT_FOLDER)) + throw new IllegalArgumentException(toChildFolder + " is not of type nt:folder"); + } else { + toChildFolder = toNode.addNode(fileName, NodeType.NT_FOLDER); + + // save session + toNode.getSession().save(); + } + count = count + copyFiles(fromChild, toChildFolder, recursive, monitor, onlyAdd); + } + } + return count; + } catch (RepositoryException e) { + throw new JcrException("Cannot copy files between " + fromNode + " and " + toNode, e); + } finally { + // in case there was an exception + // IOUtils.closeQuietly(in); + // closeQuietly(binary); + } + } + + /** + * Iteratively count all file nodes in subtree, inefficient but can be useful + * when query are poorly supported, such as in remoting. + */ + public static Long countFiles(Node node) { + Long localCount = 0l; + try { + for (NodeIterator nit = node.getNodes(); nit.hasNext();) { + Node child = nit.nextNode(); + if (child.isNodeType(NodeType.NT_FOLDER)) + localCount = localCount + countFiles(child); + else if (child.isNodeType(NodeType.NT_FILE)) + localCount = localCount + 1; + } + } catch (RepositoryException e) { + throw new JcrException("Cannot count all children of " + node, e); + } + return localCount; + } + + /** + * Copy a file as an nt:file, assuming an nt:folder hierarchy. The session is + * NOT saved. + * + * @return the created file node + */ + @Deprecated + public static Node copyFile(Node folderNode, File file) { + try (InputStream in = new FileInputStream(file)) { + return copyStreamAsFile(folderNode, file.getName(), in); + } catch (IOException e) { + throw new RuntimeException("Cannot copy file " + file + " under " + folderNode, e); + } + } + + /** Copy bytes as an nt:file */ + public static Node copyBytesAsFile(Node folderNode, String fileName, byte[] bytes) { + // InputStream in = null; + try (InputStream in = new ByteArrayInputStream(bytes)) { + // in = new ByteArrayInputStream(bytes); + return copyStreamAsFile(folderNode, fileName, in); + } catch (IOException e) { + throw new RuntimeException("Cannot copy file " + fileName + " under " + folderNode, e); + // } finally { + // IOUtils.closeQuietly(in); + } + } + + /** + * Copy a stream as an nt:file, assuming an nt:folder hierarchy. The session is + * NOT saved. + * + * @return the created file node + */ + public static Node copyStreamAsFile(Node folderNode, String fileName, InputStream in) { + Binary binary = null; + try { + Node fileNode; + Node contentNode; + if (folderNode.hasNode(fileName)) { + fileNode = folderNode.getNode(fileName); + if (!fileNode.isNodeType(NodeType.NT_FILE)) + throw new IllegalArgumentException(fileNode + " is not of type nt:file"); + // we assume that the content node is already there + contentNode = fileNode.getNode(Node.JCR_CONTENT); + } else { + fileNode = folderNode.addNode(fileName, NodeType.NT_FILE); + contentNode = fileNode.addNode(Node.JCR_CONTENT, NodeType.NT_UNSTRUCTURED); + } + binary = contentNode.getSession().getValueFactory().createBinary(in); + contentNode.setProperty(Property.JCR_DATA, binary); + updateLastModified(contentNode); + return fileNode; + } catch (RepositoryException e) { + throw new JcrException("Cannot create file node " + fileName + " under " + folderNode, e); + } finally { + closeQuietly(binary); + } + } + + /** Read an an nt:file as an {@link InputStream}. */ + public static InputStream getFileAsStream(Node fileNode) throws RepositoryException { + return fileNode.getNode(Node.JCR_CONTENT).getProperty(Property.JCR_DATA).getBinary().getStream(); + } + + /** + * Set the properties of {@link NodeType#MIX_MIMETYPE} on the content of this + * file node. + */ + public static void setFileMimeType(Node fileNode, String mimeType, String encoding) throws RepositoryException { + Node contentNode = fileNode.getNode(Node.JCR_CONTENT); + if (mimeType != null) + contentNode.setProperty(Property.JCR_MIMETYPE, mimeType); + if (encoding != null) + contentNode.setProperty(Property.JCR_ENCODING, encoding); + // TODO remove properties if args are null? + } + + public static void copyFilesToFs(Node baseNode, Path targetDir, boolean recursive) { + try { + Files.createDirectories(targetDir); + for (NodeIterator nit = baseNode.getNodes(); nit.hasNext();) { + Node node = nit.nextNode(); + if (node.isNodeType(NodeType.NT_FILE)) { + Path filePath = targetDir.resolve(node.getName()); + try (OutputStream out = Files.newOutputStream(filePath); InputStream in = getFileAsStream(node)) { + IOUtils.copy(in, out); + } + } else if (recursive && node.isNodeType(NodeType.NT_FOLDER)) { + Path dirPath = targetDir.resolve(node.getName()); + copyFilesToFs(node, dirPath, true); + } + } + } catch (RepositoryException e) { + throw new JcrException("Cannot copy " + baseNode + " to " + targetDir, e); + } catch (IOException e) { + throw new RuntimeException("Cannot copy " + baseNode + " to " + targetDir, e); + } + } + + /** + * Computes the checksum of an nt:file. + * + * @deprecated use separate digest utilities + */ + @Deprecated + public static String checksumFile(Node fileNode, String algorithm) { + try (InputStream in = fileNode.getNode(Node.JCR_CONTENT).getProperty(Property.JCR_DATA).getBinary() + .getStream()) { + return digest(algorithm, in); + } catch (IOException e) { + throw new RuntimeException("Cannot checksum file " + fileNode + " with algorithm " + algorithm, e); + } catch (RepositoryException e) { + throw new JcrException("Cannot checksum file " + fileNode + " with algorithm " + algorithm, e); + } + } + + @Deprecated + private static String digest(String algorithm, InputStream in) { + final Integer byteBufferCapacity = 100 * 1024;// 100 KB + try { + MessageDigest digest = MessageDigest.getInstance(algorithm); + byte[] buffer = new byte[byteBufferCapacity]; + int read = 0; + while ((read = in.read(buffer)) > 0) { + digest.update(buffer, 0, read); + } + + byte[] checksum = digest.digest(); + String res = encodeHexString(checksum); + return res; + } catch (IOException e) { + throw new RuntimeException("Cannot digest with algorithm " + algorithm, e); + } catch (NoSuchAlgorithmException e) { + throw new IllegalArgumentException("Cannot digest with algorithm " + algorithm, e); + } + } + + /** + * From + * http://stackoverflow.com/questions/9655181/how-to-convert-a-byte-array-to + * -a-hex-string-in-java + */ + @Deprecated + private static String encodeHexString(byte[] bytes) { + final char[] hexArray = "0123456789abcdef".toCharArray(); + char[] hexChars = new char[bytes.length * 2]; + for (int j = 0; j < bytes.length; j++) { + int v = bytes[j] & 0xFF; + hexChars[j * 2] = hexArray[v >>> 4]; + hexChars[j * 2 + 1] = hexArray[v & 0x0F]; + } + return new String(hexChars); + } + + /** Export a subtree as a compact XML without namespaces. */ + public static void toSimpleXml(Node node, StringBuilder sb) throws RepositoryException { + sb.append('<'); + String nodeName = node.getName(); + int colIndex = nodeName.indexOf(':'); + if (colIndex > 0) { + nodeName = nodeName.substring(colIndex + 1); + } + sb.append(nodeName); + PropertyIterator pit = node.getProperties(); + properties: while (pit.hasNext()) { + Property p = pit.nextProperty(); + // skip multiple properties + if (p.isMultiple()) + continue properties; + String propertyName = p.getName(); + int pcolIndex = propertyName.indexOf(':'); + // skip properties with namespaces + if (pcolIndex > 0) + continue properties; + // skip binaries + if (p.getType() == PropertyType.BINARY) { + continue properties; + // TODO retrieve identifier? + } + sb.append(' '); + sb.append(propertyName); + sb.append('='); + sb.append('\"').append(p.getString()).append('\"'); + } + + if (node.hasNodes()) { + sb.append('>'); + NodeIterator children = node.getNodes(); + while (children.hasNext()) { + toSimpleXml(children.nextNode(), sb); + } + sb.append("'); + } else { + sb.append("/>"); + } + } + +} diff --git a/org.argeo.cms.jcr/src/org/argeo/jcr/JcrxApi.java b/org.argeo.cms.jcr/src/org/argeo/jcr/JcrxApi.java new file mode 100644 index 000000000..666b2593e --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/jcr/JcrxApi.java @@ -0,0 +1,190 @@ +package org.argeo.jcr; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import javax.jcr.Node; +import javax.jcr.Property; +import javax.jcr.RepositoryException; +import javax.jcr.Value; + +/** Uilities around the JCR extensions. */ +public class JcrxApi { + public final static String MD5 = "MD5"; + public final static String SHA1 = "SHA1"; + public final static String SHA256 = "SHA-256"; + public final static String SHA512 = "SHA-512"; + + public final static String EMPTY_MD5 = "d41d8cd98f00b204e9800998ecf8427e"; + public final static String EMPTY_SHA1 = "da39a3ee5e6b4b0d3255bfef95601890afd80709"; + public final static String EMPTY_SHA256 = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"; + public final static String EMPTY_SHA512 = "cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e"; + + public final static int LENGTH_MD5 = EMPTY_MD5.length(); + public final static int LENGTH_SHA1 = EMPTY_SHA1.length(); + public final static int LENGTH_SHA256 = EMPTY_SHA256.length(); + public final static int LENGTH_SHA512 = EMPTY_SHA512.length(); + + /* + * XML + */ + /** + * Get the XML text of this child node. + */ + public static String getXmlValue(Node node, String name) { + try { + if (!node.hasNode(name)) + return null; + Node child = node.getNode(name); + return getXmlValue(child); + } catch (RepositoryException e) { + throw new IllegalStateException("Cannot get " + name + " as XML text", e); + } + } + + /** + * Get the XML text of this node. + */ + public static String getXmlValue(Node node) { + try { + if (!node.hasNode(Jcr.JCR_XMLTEXT)) + return null; + Node xmlText = node.getNode(Jcr.JCR_XMLTEXT); + if (!xmlText.hasProperty(Jcr.JCR_XMLCHARACTERS)) + throw new IllegalArgumentException( + "Node " + xmlText + " has no " + Jcr.JCR_XMLCHARACTERS + " property"); + return xmlText.getProperty(Jcr.JCR_XMLCHARACTERS).getString(); + } catch (RepositoryException e) { + throw new IllegalStateException("Cannot get " + node + " as XML text", e); + } + } + + /** + * Set as a subnode which will be exported as an XML element. + */ + public static void setXmlValue(Node node, String name, String value) { + try { + if (node.hasNode(name)) { + Node child = node.getNode(name); + setXmlValue(node, child, value); + } else + node.addNode(name, JcrxType.JCRX_XMLVALUE).addNode(Jcr.JCR_XMLTEXT, JcrxType.JCRX_XMLTEXT) + .setProperty(Jcr.JCR_XMLCHARACTERS, value); + } catch (RepositoryException e) { + throw new JcrException("Cannot set " + name + " as XML text", e); + } + } + + public static void setXmlValue(Node node, Node child, String value) { + try { + if (!child.hasNode(Jcr.JCR_XMLTEXT)) + child.addNode(Jcr.JCR_XMLTEXT, JcrxType.JCRX_XMLTEXT); + child.getNode(Jcr.JCR_XMLTEXT).setProperty(Jcr.JCR_XMLCHARACTERS, value); + } catch (RepositoryException e) { + throw new JcrException("Cannot set " + child + " as XML text", e); + } + } + + /** + * Add a checksum replacing the one which was previously set with the same + * length. + */ + public static void addChecksum(Node node, String checksum) { + try { + if (!node.hasProperty(JcrxName.JCRX_SUM)) { + node.setProperty(JcrxName.JCRX_SUM, new String[] { checksum }); + return; + } else { + int stringLength = checksum.length(); + Property property = node.getProperty(JcrxName.JCRX_SUM); + List values = Arrays.asList(property.getValues()); + Integer indexToRemove = null; + values: for (int i = 0; i < values.size(); i++) { + Value value = values.get(i); + if (value.getString().length() == stringLength) { + indexToRemove = i; + break values; + } + } + if (indexToRemove != null) + values.set(indexToRemove, node.getSession().getValueFactory().createValue(checksum)); + else + values.add(0, node.getSession().getValueFactory().createValue(checksum)); + property.setValue(values.toArray(new Value[values.size()])); + } + } catch (RepositoryException e) { + throw new JcrException("Cannot set checksum on " + node, e); + } + } + + /** Replace all checksums. */ + public static void setChecksums(Node node, List checksums) { + try { + node.setProperty(JcrxName.JCRX_SUM, checksums.toArray(new String[checksums.size()])); + } catch (RepositoryException e) { + throw new JcrException("Cannot set checksums on " + node, e); + } + } + + /** Replace all checksums. */ + public static List getChecksums(Node node) { + try { + List res = new ArrayList<>(); + if (!node.hasProperty(JcrxName.JCRX_SUM)) + return res; + Property property = node.getProperty(JcrxName.JCRX_SUM); + for (Value value : property.getValues()) { + res.add(value.getString()); + } + return res; + } catch (RepositoryException e) { + throw new JcrException("Cannot get checksums from " + node, e); + } + } + +// /** Replace all checksums with this single one. */ +// public static void setChecksum(Node node, String checksum) { +// setChecksums(node, Collections.singletonList(checksum)); +// } + + /** Retrieves the checksum with this algorithm, or null if not found. */ + public static String getChecksum(Node node, String algorithm) { + int stringLength; + switch (algorithm) { + case MD5: + stringLength = LENGTH_MD5; + break; + case SHA1: + stringLength = LENGTH_SHA1; + break; + case SHA256: + stringLength = LENGTH_SHA256; + break; + case SHA512: + stringLength = LENGTH_SHA512; + break; + default: + throw new IllegalArgumentException("Unkown algorithm " + algorithm); + } + return getChecksum(node, stringLength); + } + + /** Retrieves the checksum with this string length, or null if not found. */ + public static String getChecksum(Node node, int stringLength) { + try { + if (!node.hasProperty(JcrxName.JCRX_SUM)) + return null; + Property property = node.getProperty(JcrxName.JCRX_SUM); + for (Value value : property.getValues()) { + String str = value.getString(); + if (str.length() == stringLength) + return str; + } + return null; + } catch (RepositoryException e) { + throw new IllegalStateException("Cannot get checksum for " + node, e); + } + } + +} diff --git a/org.argeo.cms.jcr/src/org/argeo/jcr/JcrxName.java b/org.argeo.cms.jcr/src/org/argeo/jcr/JcrxName.java new file mode 100644 index 000000000..9dd43adce --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/jcr/JcrxName.java @@ -0,0 +1,7 @@ +package org.argeo.jcr; + +/** Names declared by the JCR extensions. */ +public interface JcrxName { + /** The multiple property holding various coherent checksums. */ + public final static String JCRX_SUM = "{http://www.argeo.org/ns/jcrx}sum"; +} diff --git a/org.argeo.cms.jcr/src/org/argeo/jcr/JcrxType.java b/org.argeo.cms.jcr/src/org/argeo/jcr/JcrxType.java new file mode 100644 index 000000000..0cbad3341 --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/jcr/JcrxType.java @@ -0,0 +1,17 @@ +package org.argeo.jcr; + +/** Node types declared by the JCR extensions. */ +public interface JcrxType { + /** + * Node type for an XML value, which will be serialized in XML as an element + * containing text. + */ + public final static String JCRX_XMLVALUE = "{http://www.argeo.org/ns/jcrx}xmlvalue"; + + /** Node type for the node containing the text. */ + public final static String JCRX_XMLTEXT = "{http://www.argeo.org/ns/jcrx}xmltext"; + + /** Mixin node type for a set of checksums. */ + public final static String JCRX_CSUM = "{http://www.argeo.org/ns/jcrx}csum"; + +} diff --git a/org.argeo.cms.jcr/src/org/argeo/jcr/PropertyDiff.java b/org.argeo.cms.jcr/src/org/argeo/jcr/PropertyDiff.java new file mode 100644 index 000000000..71e76fe9b --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/jcr/PropertyDiff.java @@ -0,0 +1,57 @@ +package org.argeo.jcr; + +import javax.jcr.Value; + +/** The result of the comparison of two JCR properties. */ +public class PropertyDiff { + public final static Integer MODIFIED = 0; + public final static Integer ADDED = 1; + public final static Integer REMOVED = 2; + + private final Integer type; + private final String relPath; + private final Value referenceValue; + private final Value newValue; + + public PropertyDiff(Integer type, String relPath, Value referenceValue, Value newValue) { + super(); + + if (type == MODIFIED) { + if (referenceValue == null || newValue == null) + throw new IllegalArgumentException("Reference and new values must be specified."); + } else if (type == ADDED) { + if (referenceValue != null || newValue == null) + throw new IllegalArgumentException("New value and only it must be specified."); + } else if (type == REMOVED) { + if (referenceValue == null || newValue != null) + throw new IllegalArgumentException("Reference value and only it must be specified."); + } else { + throw new IllegalArgumentException("Unkown diff type " + type); + } + + if (relPath == null) + throw new IllegalArgumentException("Relative path must be specified"); + + this.type = type; + this.relPath = relPath; + this.referenceValue = referenceValue; + this.newValue = newValue; + } + + public Integer getType() { + return type; + } + + public String getRelPath() { + return relPath; + } + + public Value getReferenceValue() { + return referenceValue; + } + + public Value getNewValue() { + return newValue; + } + +} diff --git a/org.argeo.cms.jcr/src/org/argeo/jcr/SimplePrincipal.java b/org.argeo.cms.jcr/src/org/argeo/jcr/SimplePrincipal.java new file mode 100644 index 000000000..4f42f2d9c --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/jcr/SimplePrincipal.java @@ -0,0 +1,43 @@ +package org.argeo.jcr; + +import java.security.Principal; + +/** Canonical implementation of a {@link Principal} */ +class SimplePrincipal implements Principal { + private final String name; + + public SimplePrincipal(String name) { + if (name == null) + throw new IllegalArgumentException("Principal name cannot be null"); + this.name = name; + } + + public String getName() { + return name; + } + + @Override + public int hashCode() { + return name.hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (obj == null) + return false; + if (obj instanceof Principal) + return name.equals((((Principal) obj).getName())); + return name.equals(obj.toString()); + } + + @Override + protected Object clone() throws CloneNotSupportedException { + return new SimplePrincipal(name); + } + + @Override + public String toString() { + return name; + } + +} diff --git a/org.argeo.cms.jcr/src/org/argeo/jcr/ThreadBoundJcrSessionFactory.java b/org.argeo.cms.jcr/src/org/argeo/jcr/ThreadBoundJcrSessionFactory.java new file mode 100644 index 000000000..1e23338b5 --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/jcr/ThreadBoundJcrSessionFactory.java @@ -0,0 +1,280 @@ +package org.argeo.jcr; + +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +import javax.jcr.LoginException; +import javax.jcr.Repository; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.SimpleCredentials; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** Proxy JCR sessions and attach them to calling threads. */ +@Deprecated +public abstract class ThreadBoundJcrSessionFactory { + private final static Log log = LogFactory.getLog(ThreadBoundJcrSessionFactory.class); + + private Repository repository; + /** can be injected as list, only used if repository is null */ + private List repositories; + + private ThreadLocal session = new ThreadLocal(); + private final Session proxiedSession; + /** If workspace is null, default will be used. */ + private String workspace = null; + + private String defaultUsername = "demo"; + private String defaultPassword = "demo"; + private Boolean forceDefaultCredentials = false; + + private boolean active = true; + + // monitoring + private final List threads = Collections.synchronizedList(new ArrayList()); + private final Map activeSessions = Collections.synchronizedMap(new HashMap()); + private MonitoringThread monitoringThread; + + public ThreadBoundJcrSessionFactory() { + Class[] interfaces = { Session.class }; + proxiedSession = (Session) Proxy.newProxyInstance(ThreadBoundJcrSessionFactory.class.getClassLoader(), + interfaces, new JcrSessionInvocationHandler()); + } + + /** Logs in to the repository using various strategies. */ + protected synchronized Session login() { + if (!isActive()) + throw new IllegalStateException("Thread bound session factory inactive"); + + // discard session previously attached to this thread + Thread thread = Thread.currentThread(); + if (activeSessions.containsKey(thread.getId())) { + Session oldSession = activeSessions.remove(thread.getId()); + oldSession.logout(); + session.remove(); + } + + Session newSession = null; + // first try to login without credentials, assuming the underlying login + // module will have dealt with authentication (typically using Spring + // Security) + if (!forceDefaultCredentials) + try { + newSession = repository().login(workspace); + } catch (LoginException e1) { + log.warn("Cannot login without credentials: " + e1.getMessage()); + // invalid credentials, go to the next step + } catch (RepositoryException e1) { + // other kind of exception, fail + throw new JcrException("Cannot log in to repository", e1); + } + + // log using default username / password (useful for testing purposes) + if (newSession == null) + try { + SimpleCredentials sc = new SimpleCredentials(defaultUsername, defaultPassword.toCharArray()); + newSession = repository().login(sc, workspace); + } catch (RepositoryException e) { + throw new JcrException("Cannot log in to repository", e); + } + + session.set(newSession); + // Log and monitor new session + if (log.isTraceEnabled()) + log.trace("Logged in to JCR session " + newSession + "; userId=" + newSession.getUserID()); + + // monitoring + activeSessions.put(thread.getId(), newSession); + threads.add(thread); + return newSession; + } + + public Object getObject() { + return proxiedSession; + } + + public void init() throws Exception { + // log.error("SHOULD NOT BE USED ANYMORE"); + monitoringThread = new MonitoringThread(); + monitoringThread.start(); + } + + public void dispose() throws Exception { + // if (activeSessions.size() == 0) + // return; + + if (log.isTraceEnabled()) + log.trace("Cleaning up " + activeSessions.size() + " active JCR sessions..."); + + deactivate(); + for (Session sess : activeSessions.values()) { + JcrUtils.logoutQuietly(sess); + } + activeSessions.clear(); + } + + protected Boolean isActive() { + return active; + } + + protected synchronized void deactivate() { + active = false; + notifyAll(); + } + + protected synchronized void removeSession(Thread thread) { + if (!isActive()) + return; + activeSessions.remove(thread.getId()); + threads.remove(thread); + } + + protected synchronized void cleanDeadThreads() { + if (!isActive()) + return; + Iterator it = threads.iterator(); + while (it.hasNext()) { + Thread thread = it.next(); + if (!thread.isAlive() && isActive()) { + if (activeSessions.containsKey(thread.getId())) { + Session session = activeSessions.get(thread.getId()); + activeSessions.remove(thread.getId()); + session.logout(); + if (log.isTraceEnabled()) + log.trace("Cleaned up JCR session (userID=" + session.getUserID() + ") from dead thread " + + thread.getId()); + } + it.remove(); + } + } + try { + wait(1000); + } catch (InterruptedException e) { + // silent + } + } + + public Class getObjectType() { + return Session.class; + } + + public boolean isSingleton() { + return true; + } + + /** + * Called before a method is actually called, allowing to check the session or + * re-login it (e.g. if authentication has changed). The default implementation + * returns the session. + */ + protected Session preCall(Session session) { + return session; + } + + protected Repository repository() { + if (repository != null) + return repository; + if (repositories != null) { + // hardened for OSGi dynamic services + Iterator it = repositories.iterator(); + if (it.hasNext()) + return it.next(); + } + throw new IllegalStateException("No repository injected"); + } + + // /** Useful for declarative registration of OSGi services (blueprint) */ + // public void register(Repository repository, Map params) { + // this.repository = repository; + // } + // + // /** Useful for declarative registration of OSGi services (blueprint) */ + // public void unregister(Repository repository, Map params) { + // this.repository = null; + // } + + public void setRepository(Repository repository) { + this.repository = repository; + } + + public void setRepositories(List repositories) { + this.repositories = repositories; + } + + public void setDefaultUsername(String defaultUsername) { + this.defaultUsername = defaultUsername; + } + + public void setDefaultPassword(String defaultPassword) { + this.defaultPassword = defaultPassword; + } + + public void setForceDefaultCredentials(Boolean forceDefaultCredentials) { + this.forceDefaultCredentials = forceDefaultCredentials; + } + + public void setWorkspace(String workspace) { + this.workspace = workspace; + } + + protected class JcrSessionInvocationHandler implements InvocationHandler { + + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable, RepositoryException { + Session threadSession = session.get(); + if (threadSession == null) { + if ("logout".equals(method.getName()))// no need to login + return Void.TYPE; + else if ("toString".equals(method.getName()))// maybe logging + return "Uninitialized Argeo thread bound JCR session"; + threadSession = login(); + } + + preCall(threadSession); + Object ret; + try { + ret = method.invoke(threadSession, args); + } catch (InvocationTargetException e) { + Throwable cause = e.getCause(); + if (cause instanceof RepositoryException) + throw (RepositoryException) cause; + else + throw cause; + } + if ("logout".equals(method.getName())) { + session.remove(); + Thread thread = Thread.currentThread(); + removeSession(thread); + if (log.isTraceEnabled()) + log.trace("Logged out JCR session (userId=" + threadSession.getUserID() + ") on thread " + + thread.getId()); + } + return ret; + } + } + + /** Monitors registered thread in order to clean up dead ones. */ + private class MonitoringThread extends Thread { + + public MonitoringThread() { + super("ThreadBound JCR Session Monitor"); + } + + @Override + public void run() { + while (isActive()) { + cleanDeadThreads(); + } + } + + } +} diff --git a/org.argeo.cms.jcr/src/org/argeo/jcr/VersionDiff.java b/org.argeo.cms.jcr/src/org/argeo/jcr/VersionDiff.java new file mode 100644 index 000000000..dab55548b --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/jcr/VersionDiff.java @@ -0,0 +1,38 @@ +package org.argeo.jcr; + +import java.util.Calendar; +import java.util.Map; + +/** + * Generic Object that enables the creation of history reports based on a JCR + * versionable node. userId and creation date are added to the map of + * PropertyDiff. + * + * These two fields might be null + * + */ +public class VersionDiff { + + private String userId; + private Map diffs; + private Calendar updateTime; + + public VersionDiff(String userId, Calendar updateTime, + Map diffs) { + this.userId = userId; + this.updateTime = updateTime; + this.diffs = diffs; + } + + public String getUserId() { + return userId; + } + + public Map getDiffs() { + return diffs; + } + + public Calendar getUpdateTime() { + return updateTime; + } +} diff --git a/org.argeo.cms.jcr/src/org/argeo/jcr/fs/BinaryChannel.java b/org.argeo.cms.jcr/src/org/argeo/jcr/fs/BinaryChannel.java new file mode 100644 index 000000000..d6550feee --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/jcr/fs/BinaryChannel.java @@ -0,0 +1,190 @@ +package org.argeo.jcr.fs; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; +import java.nio.channels.Channels; +import java.nio.channels.FileChannel; +import java.nio.channels.ReadableByteChannel; +import java.nio.channels.SeekableByteChannel; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; + +import javax.jcr.Binary; +import javax.jcr.Node; +import javax.jcr.Property; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.nodetype.NodeType; + +import org.argeo.jcr.JcrUtils; + +/** A read/write {@link SeekableByteChannel} based on a {@link Binary}. */ +public class BinaryChannel implements SeekableByteChannel { + private final Node file; + private Binary binary; + private boolean open = true; + + private long position = 0; + + private FileChannel fc = null; + + public BinaryChannel(Node file, Path path) throws RepositoryException, IOException { + this.file = file; + Session session = file.getSession(); + synchronized (session) { + if (file.isNodeType(NodeType.NT_FILE)) { + if (file.hasNode(Node.JCR_CONTENT)) { + Node data = file.getNode(Property.JCR_CONTENT); + this.binary = data.getProperty(Property.JCR_DATA).getBinary(); + } else { + Node data = file.addNode(Node.JCR_CONTENT, NodeType.NT_UNSTRUCTURED); + data.addMixin(NodeType.MIX_LAST_MODIFIED); + try (InputStream in = new ByteArrayInputStream(new byte[0])) { + this.binary = data.getSession().getValueFactory().createBinary(in); + } + data.setProperty(Property.JCR_DATA, this.binary); + + // MIME type + String mime = Files.probeContentType(path); + // String mime = fileTypeMap.getContentType(file.getName()); + data.setProperty(Property.JCR_MIMETYPE, mime); + + session.refresh(true); + session.save(); + session.notifyAll(); + } + } else { + throw new IllegalArgumentException( + "Unsupported file node " + file + " (" + file.getPrimaryNodeType() + ")"); + } + } + } + + @Override + public synchronized boolean isOpen() { + return open; + } + + @Override + public synchronized void close() throws IOException { + if (isModified()) { + Binary newBinary = null; + try { + Session session = file.getSession(); + synchronized (session) { + fc.position(0); + InputStream in = Channels.newInputStream(fc); + newBinary = session.getValueFactory().createBinary(in); + file.getNode(Property.JCR_CONTENT).setProperty(Property.JCR_DATA, newBinary); + session.refresh(true); + session.save(); + open = false; + session.notifyAll(); + } + } catch (RepositoryException e) { + throw new IOException("Cannot close " + file, e); + } finally { + JcrUtils.closeQuietly(newBinary); + // IOUtils.closeQuietly(fc); + if (fc != null) { + fc.close(); + } + } + } else { + clearReadState(); + open = false; + } + } + + @Override + public int read(ByteBuffer dst) throws IOException { + if (isModified()) { + return fc.read(dst); + } else { + + try { + int read; + byte[] arr = dst.array(); + read = binary.read(arr, position); + + if (read != -1) + position = position + read; + return read; + } catch (RepositoryException e) { + throw new IOException("Cannot read into buffer", e); + } + } + } + + @Override + public int write(ByteBuffer src) throws IOException { + int written = getFileChannel().write(src); + return written; + } + + @Override + public long position() throws IOException { + if (isModified()) + return getFileChannel().position(); + else + return position; + } + + @Override + public SeekableByteChannel position(long newPosition) throws IOException { + if (isModified()) { + getFileChannel().position(position); + } else { + this.position = newPosition; + } + return this; + } + + @Override + public long size() throws IOException { + if (isModified()) { + return getFileChannel().size(); + } else { + try { + return binary.getSize(); + } catch (RepositoryException e) { + throw new IOException("Cannot get size", e); + } + } + } + + @Override + public SeekableByteChannel truncate(long size) throws IOException { + getFileChannel().truncate(size); + return this; + } + + private FileChannel getFileChannel() throws IOException { + try { + if (fc == null) { + Path tempPath = Files.createTempFile(getClass().getSimpleName(), null); + fc = FileChannel.open(tempPath, StandardOpenOption.WRITE, StandardOpenOption.READ, + StandardOpenOption.DELETE_ON_CLOSE, StandardOpenOption.SPARSE); + ReadableByteChannel readChannel = Channels.newChannel(binary.getStream()); + fc.transferFrom(readChannel, 0, binary.getSize()); + clearReadState(); + } + return fc; + } catch (RepositoryException e) { + throw new IOException("Cannot get temp file channel", e); + } + } + + private boolean isModified() { + return fc != null; + } + + private void clearReadState() { + position = -1; + JcrUtils.closeQuietly(binary); + binary = null; + } +} diff --git a/org.argeo.cms.jcr/src/org/argeo/jcr/fs/JcrBasicfileAttributes.java b/org.argeo.cms.jcr/src/org/argeo/jcr/fs/JcrBasicfileAttributes.java new file mode 100644 index 000000000..7c9711bf0 --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/jcr/fs/JcrBasicfileAttributes.java @@ -0,0 +1,138 @@ +package org.argeo.jcr.fs; + +import static javax.jcr.Property.JCR_CREATED; +import static javax.jcr.Property.JCR_LAST_MODIFIED; + +import java.nio.file.attribute.FileTime; +import java.time.Instant; + +import javax.jcr.Binary; +import javax.jcr.Node; +import javax.jcr.Property; +import javax.jcr.RepositoryException; +import javax.jcr.nodetype.NodeType; + +import org.argeo.jcr.JcrUtils; + +public class JcrBasicfileAttributes implements NodeFileAttributes { + private final Node node; + + private final static FileTime EPOCH = FileTime.fromMillis(0); + + public JcrBasicfileAttributes(Node node) { + if (node == null) + throw new JcrFsException("Node underlying the attributes cannot be null"); + this.node = node; + } + + @Override + public FileTime lastModifiedTime() { + try { + if (node.hasProperty(JCR_LAST_MODIFIED)) { + Instant instant = node.getProperty(JCR_LAST_MODIFIED).getDate().toInstant(); + return FileTime.from(instant); + } else if (node.hasProperty(JCR_CREATED)) { + Instant instant = node.getProperty(JCR_CREATED).getDate().toInstant(); + return FileTime.from(instant); + } +// if (node.isNodeType(NodeType.MIX_LAST_MODIFIED)) { +// Instant instant = node.getProperty(Property.JCR_LAST_MODIFIED).getDate().toInstant(); +// return FileTime.from(instant); +// } + return EPOCH; + } catch (RepositoryException e) { + throw new JcrFsException("Cannot get last modified time", e); + } + } + + @Override + public FileTime lastAccessTime() { + return lastModifiedTime(); + } + + @Override + public FileTime creationTime() { + try { + if (node.hasProperty(JCR_CREATED)) { + Instant instant = node.getProperty(JCR_CREATED).getDate().toInstant(); + return FileTime.from(instant); + } else if (node.hasProperty(JCR_LAST_MODIFIED)) { + Instant instant = node.getProperty(JCR_LAST_MODIFIED).getDate().toInstant(); + return FileTime.from(instant); + } +// if (node.isNodeType(NodeType.MIX_CREATED)) { +// Instant instant = node.getProperty(JCR_CREATED).getDate().toInstant(); +// return FileTime.from(instant); +// } + return EPOCH; + } catch (RepositoryException e) { + throw new JcrFsException("Cannot get creation time", e); + } + } + + @Override + public boolean isRegularFile() { + try { + return node.isNodeType(NodeType.NT_FILE); + } catch (RepositoryException e) { + throw new JcrFsException("Cannot check if regular file", e); + } + } + + @Override + public boolean isDirectory() { + try { + if (node.isNodeType(NodeType.NT_FOLDER)) + return true; + // all other non file nodes + return !(node.isNodeType(NodeType.NT_FILE) || node.isNodeType(NodeType.NT_LINKED_FILE)); + } catch (RepositoryException e) { + throw new JcrFsException("Cannot check if directory", e); + } + } + + @Override + public boolean isSymbolicLink() { + try { + return node.isNodeType(NodeType.NT_LINKED_FILE); + } catch (RepositoryException e) { + throw new JcrFsException("Cannot check if linked file", e); + } + } + + @Override + public boolean isOther() { + return !(isDirectory() || isRegularFile() || isSymbolicLink()); + } + + @Override + public long size() { + if (isRegularFile()) { + Binary binary = null; + try { + binary = node.getNode(Property.JCR_CONTENT).getProperty(Property.JCR_DATA).getBinary(); + return binary.getSize(); + } catch (RepositoryException e) { + throw new JcrFsException("Cannot check size", e); + } finally { + JcrUtils.closeQuietly(binary); + } + } + return -1; + } + + @Override + public Object fileKey() { + try { + return node.getIdentifier(); + } catch (RepositoryException e) { + throw new JcrFsException("Cannot get identifier", e); + } + } + + @Override + public Node getNode() { + return node; + } + +} diff --git a/org.argeo.cms.jcr/src/org/argeo/jcr/fs/JcrFileSystem.java b/org.argeo.cms.jcr/src/org/argeo/jcr/fs/JcrFileSystem.java new file mode 100644 index 000000000..3d538e8bd --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/jcr/fs/JcrFileSystem.java @@ -0,0 +1,250 @@ +package org.argeo.jcr.fs; + +import java.io.IOException; +import java.nio.file.FileStore; +import java.nio.file.FileSystem; +import java.nio.file.Path; +import java.nio.file.PathMatcher; +import java.nio.file.WatchService; +import java.nio.file.attribute.UserPrincipalLookupService; +import java.nio.file.spi.FileSystemProvider; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; + +import javax.jcr.Credentials; +import javax.jcr.Node; +import javax.jcr.Repository; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.nodetype.NodeType; + +import org.argeo.jcr.Jcr; +import org.argeo.jcr.JcrUtils; + +public class JcrFileSystem extends FileSystem { + private final JcrFileSystemProvider provider; + + private final Repository repository; + private Session session; + private WorkspaceFileStore baseFileStore; + + private Map mounts = new TreeMap<>(); + + private String userHomePath = null; + + @Deprecated + public JcrFileSystem(JcrFileSystemProvider provider, Session session) throws IOException { + super(); + this.provider = provider; + baseFileStore = new WorkspaceFileStore(null, session.getWorkspace()); + this.session = session; +// Node userHome = provider.getUserHome(session); +// if (userHome != null) +// try { +// userHomePath = userHome.getPath(); +// } catch (RepositoryException e) { +// throw new IOException("Cannot retrieve user home path", e); +// } + this.repository = null; + } + + public JcrFileSystem(JcrFileSystemProvider provider, Repository repository) throws IOException { + this(provider, repository, null); + } + + public JcrFileSystem(JcrFileSystemProvider provider, Repository repository, Credentials credentials) + throws IOException { + super(); + this.provider = provider; + this.repository = repository; + try { + this.session = credentials == null ? repository.login() : repository.login(credentials); + baseFileStore = new WorkspaceFileStore(null, session.getWorkspace()); + workspaces: for (String workspaceName : baseFileStore.getWorkspace().getAccessibleWorkspaceNames()) { + if (workspaceName.equals(baseFileStore.getWorkspace().getName())) + continue workspaces;// do not mount base + if (workspaceName.equals("security")) { + continue workspaces;// do not mount security workspace + // TODO make it configurable + } + Session mountSession = credentials == null ? repository.login(workspaceName) + : repository.login(credentials, workspaceName); + String mountPath = JcrPath.separator + workspaceName; + mounts.put(mountPath, new WorkspaceFileStore(mountPath, mountSession.getWorkspace())); + } + } catch (RepositoryException e) { + throw new IOException("Cannot initialise file system", e); + } + + Node userHome = provider.getUserHome(repository); + if (userHome != null) + try { + userHomePath = toFsPath(userHome); + } catch (RepositoryException e) { + throw new IOException("Cannot retrieve user home path", e); + } finally { + JcrUtils.logoutQuietly(Jcr.session(userHome)); + } + } + + public String toFsPath(Node node) throws RepositoryException { + return getFileStore(node).toFsPath(node); + } + + /** Whether this node should be skipped in directory listings */ + public boolean skipNode(Node node) throws RepositoryException { + if (node.isNodeType(NodeType.NT_HIERARCHY_NODE)) + return false; + return true; + } + + public String getUserHomePath() { + return userHomePath; + } + + public WorkspaceFileStore getFileStore(String path) { + WorkspaceFileStore res = baseFileStore; + for (String mountPath : mounts.keySet()) { + if (path.equals(mountPath)) + return mounts.get(mountPath); + if (path.startsWith(mountPath + JcrPath.separator)) { + res = mounts.get(mountPath); + // we keep the last one + } + } + assert res != null; + return res; + } + + public WorkspaceFileStore getFileStore(Node node) throws RepositoryException { + String workspaceName = node.getSession().getWorkspace().getName(); + if (workspaceName.equals(baseFileStore.getWorkspace().getName())) + return baseFileStore; + for (String mountPath : mounts.keySet()) { + WorkspaceFileStore fileStore = mounts.get(mountPath); + if (workspaceName.equals(fileStore.getWorkspace().getName())) + return fileStore; + } + throw new IllegalStateException("No workspace mount found for " + node + " in workspace " + workspaceName); + } + + public Iterator listDirectMounts(Path base) { + String baseStr = base.toString(); + Set res = new HashSet<>(); + mounts: for (String mountPath : mounts.keySet()) { + if (mountPath.equals(baseStr)) + continue mounts; + if (mountPath.startsWith(baseStr)) { + JcrPath path = new JcrPath(this, mountPath); + Path relPath = base.relativize(path); + if (relPath.getNameCount() == 1) + res.add(path); + } + } + return res.iterator(); + } + + public WorkspaceFileStore getBaseFileStore() { + return baseFileStore; + } + + @Override + public FileSystemProvider provider() { + return provider; + } + + @Override + public void close() throws IOException { + JcrUtils.logoutQuietly(session); + for (String mountPath : mounts.keySet()) { + WorkspaceFileStore fileStore = mounts.get(mountPath); + try { + fileStore.close(); + } catch (Exception e) { + e.printStackTrace(); + } + } + } + + @Override + public boolean isOpen() { + return session.isLive(); + } + + @Override + public boolean isReadOnly() { + return false; + } + + @Override + public String getSeparator() { + return JcrPath.separator; + } + + @Override + public Iterable getRootDirectories() { + Set single = new HashSet<>(); + single.add(new JcrPath(this, JcrPath.separator)); + return single; + } + + @Override + public Iterable getFileStores() { + List stores = new ArrayList<>(); + stores.add(baseFileStore); + stores.addAll(mounts.values()); + return stores; + } + + @Override + public Set supportedFileAttributeViews() { + try { + String[] prefixes = session.getNamespacePrefixes(); + Set res = new HashSet<>(); + for (String prefix : prefixes) + res.add(prefix); + res.add("basic"); + return res; + } catch (RepositoryException e) { + throw new JcrFsException("Cannot get supported file attributes views", e); + } + } + + @Override + public Path getPath(String first, String... more) { + StringBuilder sb = new StringBuilder(first); + // TODO Make it more robust + for (String part : more) + sb.append('/').append(part); + return new JcrPath(this, sb.toString()); + } + + @Override + public PathMatcher getPathMatcher(String syntaxAndPattern) { + throw new UnsupportedOperationException(); + } + + @Override + public UserPrincipalLookupService getUserPrincipalLookupService() { + throw new UnsupportedOperationException(); + } + + @Override + public WatchService newWatchService() throws IOException { + throw new UnsupportedOperationException(); + } + +// public Session getSession() { +// return session; +// } + + public Repository getRepository() { + return repository; + } + +} diff --git a/org.argeo.cms.jcr/src/org/argeo/jcr/fs/JcrFileSystemProvider.java b/org.argeo.cms.jcr/src/org/argeo/jcr/fs/JcrFileSystemProvider.java new file mode 100644 index 000000000..74d9a198e --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/jcr/fs/JcrFileSystemProvider.java @@ -0,0 +1,337 @@ +package org.argeo.jcr.fs; + +import java.io.IOException; +import java.nio.channels.SeekableByteChannel; +import java.nio.file.AccessMode; +import java.nio.file.CopyOption; +import java.nio.file.DirectoryNotEmptyException; +import java.nio.file.DirectoryStream; +import java.nio.file.DirectoryStream.Filter; +import java.nio.file.FileStore; +import java.nio.file.LinkOption; +import java.nio.file.NoSuchFileException; +import java.nio.file.OpenOption; +import java.nio.file.Path; +import java.nio.file.attribute.BasicFileAttributes; +import java.nio.file.attribute.FileAttribute; +import java.nio.file.attribute.FileAttributeView; +import java.nio.file.spi.FileSystemProvider; +import java.util.Calendar; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +import javax.jcr.Node; +import javax.jcr.Property; +import javax.jcr.PropertyIterator; +import javax.jcr.PropertyType; +import javax.jcr.Repository; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.nodetype.NodeType; +import javax.jcr.nodetype.PropertyDefinition; + +import org.argeo.jcr.JcrUtils; + +/** Operations on a {@link JcrFileSystem}. */ +public abstract class JcrFileSystemProvider extends FileSystemProvider { + + @Override + public SeekableByteChannel newByteChannel(Path path, Set options, FileAttribute... attrs) + throws IOException { + Node node = toNode(path); + try { + if (node == null) { + Node parent = toNode(path.getParent()); + if (parent == null) + throw new IOException("No parent directory for " + path); + if (parent.getPrimaryNodeType().isNodeType(NodeType.NT_FILE) + || parent.getPrimaryNodeType().isNodeType(NodeType.NT_LINKED_FILE)) + throw new IOException(path + " parent is a file"); + + String fileName = path.getFileName().toString(); + fileName = Text.escapeIllegalJcrChars(fileName); + node = parent.addNode(fileName, NodeType.NT_FILE); + node.addMixin(NodeType.MIX_CREATED); +// node.addMixin(NodeType.MIX_LAST_MODIFIED); + } + if (!node.isNodeType(NodeType.NT_FILE)) + throw new UnsupportedOperationException(node + " must be a file"); + return new BinaryChannel(node, path); + } catch (RepositoryException e) { + discardChanges(node); + throw new IOException("Cannot read file", e); + } + } + + @Override + public DirectoryStream newDirectoryStream(Path dir, Filter filter) throws IOException { + try { + Node base = toNode(dir); + if (base == null) + throw new IOException(dir + " is not a JCR node"); + JcrFileSystem fileSystem = (JcrFileSystem) dir.getFileSystem(); + return new NodeDirectoryStream(fileSystem, base.getNodes(), fileSystem.listDirectMounts(dir), filter); + } catch (RepositoryException e) { + throw new IOException("Cannot list directory", e); + } + } + + @Override + public void createDirectory(Path dir, FileAttribute... attrs) throws IOException { + Node node = toNode(dir); + try { + if (node == null) { + Node parent = toNode(dir.getParent()); + if (parent == null) + throw new IOException("Parent of " + dir + " does not exist"); + Session session = parent.getSession(); + synchronized (session) { + if (parent.getPrimaryNodeType().isNodeType(NodeType.NT_FILE) + || parent.getPrimaryNodeType().isNodeType(NodeType.NT_LINKED_FILE)) + throw new IOException(dir + " parent is a file"); + String fileName = dir.getFileName().toString(); + fileName = Text.escapeIllegalJcrChars(fileName); + node = parent.addNode(fileName, NodeType.NT_FOLDER); + node.addMixin(NodeType.MIX_CREATED); + node.addMixin(NodeType.MIX_LAST_MODIFIED); + save(session); + } + } else { + // if (!node.getPrimaryNodeType().isNodeType(NodeType.NT_FOLDER)) + // throw new FileExistsException(dir + " exists and is not a directory"); + } + } catch (RepositoryException e) { + discardChanges(node); + throw new IOException("Cannot create directory " + dir, e); + } + } + + @Override + public void delete(Path path) throws IOException { + Node node = toNode(path); + try { + if (node == null) + throw new NoSuchFileException(path + " does not exist"); + Session session = node.getSession(); + synchronized (session) { + session.refresh(false); + if (node.getPrimaryNodeType().isNodeType(NodeType.NT_FILE)) + node.remove(); + else if (node.getPrimaryNodeType().isNodeType(NodeType.NT_FOLDER)) { + if (node.hasNodes())// TODO check only files + throw new DirectoryNotEmptyException(path.toString()); + node.remove(); + } + save(session); + } + } catch (RepositoryException e) { + discardChanges(node); + throw new IOException("Cannot delete " + path, e); + } + + } + + @Override + public void copy(Path source, Path target, CopyOption... options) throws IOException { + Node sourceNode = toNode(source); + Node targetNode = toNode(target); + try { + Session targetSession = targetNode.getSession(); + synchronized (targetSession) { + JcrUtils.copy(sourceNode, targetNode); + save(targetSession); + } + } catch (RepositoryException e) { + discardChanges(sourceNode); + discardChanges(targetNode); + throw new IOException("Cannot copy from " + source + " to " + target, e); + } + } + + @Override + public void move(Path source, Path target, CopyOption... options) throws IOException { + JcrFileSystem sourceFileSystem = (JcrFileSystem) source.getFileSystem(); + WorkspaceFileStore sourceStore = sourceFileSystem.getFileStore(source.toString()); + WorkspaceFileStore targetStore = sourceFileSystem.getFileStore(target.toString()); + try { + if (sourceStore.equals(targetStore)) { + sourceStore.getWorkspace().move(sourceStore.toJcrPath(source.toString()), + targetStore.toJcrPath(target.toString())); + } else { + // TODO implement it + throw new UnsupportedOperationException("Can only move paths within the same workspace."); + } + } catch (RepositoryException e) { + throw new IOException("Cannot move from " + source + " to " + target, e); + } + +// Node sourceNode = toNode(source); +// try { +// Session session = sourceNode.getSession(); +// synchronized (session) { +// session.move(sourceNode.getPath(), target.toString()); +// save(session); +// } +// } catch (RepositoryException e) { +// discardChanges(sourceNode); +// throw new IOException("Cannot move from " + source + " to " + target, e); +// } + } + + @Override + public boolean isSameFile(Path path, Path path2) throws IOException { + if (path.getFileSystem() != path2.getFileSystem()) + return false; + boolean equ = path.equals(path2); + if (equ) + return true; + else { + try { + Node node = toNode(path); + Node node2 = toNode(path2); + return node.isSame(node2); + } catch (RepositoryException e) { + throw new IOException("Cannot check whether " + path + " and " + path2 + " are same", e); + } + } + + } + + @Override + public boolean isHidden(Path path) throws IOException { + return path.getFileName().toString().charAt(0) == '.'; + } + + @Override + public FileStore getFileStore(Path path) throws IOException { + JcrFileSystem fileSystem = (JcrFileSystem) path.getFileSystem(); + return fileSystem.getFileStore(path.toString()); + } + + @Override + public void checkAccess(Path path, AccessMode... modes) throws IOException { + Node node = toNode(path); + if (node == null) + throw new NoSuchFileException(path + " does not exist"); + // TODO check access via JCR api + } + + @Override + public V getFileAttributeView(Path path, Class type, LinkOption... options) { + throw new UnsupportedOperationException(); + } + + @SuppressWarnings("unchecked") + @Override + public A readAttributes(Path path, Class type, LinkOption... options) + throws IOException { + // TODO check if assignable + Node node = toNode(path); + if (node == null) { + throw new IOException("JCR node not found for " + path); + } + return (A) new JcrBasicfileAttributes(node); + } + + @Override + public Map readAttributes(Path path, String attributes, LinkOption... options) throws IOException { + try { + Node node = toNode(path); + String pattern = attributes.replace(',', '|'); + Map res = new HashMap(); + PropertyIterator it = node.getProperties(pattern); + props: while (it.hasNext()) { + Property prop = it.nextProperty(); + PropertyDefinition pd = prop.getDefinition(); + if (pd.isMultiple()) + continue props; + int requiredType = pd.getRequiredType(); + switch (requiredType) { + case PropertyType.LONG: + res.put(prop.getName(), prop.getLong()); + break; + case PropertyType.DOUBLE: + res.put(prop.getName(), prop.getDouble()); + break; + case PropertyType.BOOLEAN: + res.put(prop.getName(), prop.getBoolean()); + break; + case PropertyType.DATE: + res.put(prop.getName(), prop.getDate()); + break; + case PropertyType.BINARY: + byte[] arr = JcrUtils.getBinaryAsBytes(prop); + res.put(prop.getName(), arr); + break; + default: + res.put(prop.getName(), prop.getString()); + } + } + return res; + } catch (RepositoryException e) { + throw new IOException("Cannot read attributes of " + path, e); + } + } + + @Override + public void setAttribute(Path path, String attribute, Object value, LinkOption... options) throws IOException { + Node node = toNode(path); + try { + Session session = node.getSession(); + synchronized (session) { + if (value instanceof byte[]) { + JcrUtils.setBinaryAsBytes(node, attribute, (byte[]) value); + } else if (value instanceof Calendar) { + node.setProperty(attribute, (Calendar) value); + } else { + node.setProperty(attribute, value.toString()); + } + save(session); + } + } catch (RepositoryException e) { + discardChanges(node); + throw new IOException("Cannot set attribute " + attribute + " on " + path, e); + } + } + + protected Node toNode(Path path) { + try { + return ((JcrPath) path).getNode(); + } catch (RepositoryException e) { + throw new JcrFsException("Cannot convert path " + path + " to JCR Node", e); + } + } + + /** Discard changes in the underlying session */ + protected void discardChanges(Node node) { + if (node == null) + return; + try { + // discard changes + node.getSession().refresh(false); + } catch (RepositoryException e) { + e.printStackTrace(); + // TODO log out session? + // TODO use Commons logging? + } + } + + /** Make sure save is robust. */ + protected void save(Session session) throws RepositoryException { + session.refresh(true); + session.save(); + session.notifyAll(); + } + + /** + * To be overriden in order to support the ~ path, with an implementation + * specific concept of user home. + * + * @return null by default + */ + public Node getUserHome(Repository session) { + return null; + } + +} diff --git a/org.argeo.cms.jcr/src/org/argeo/jcr/fs/JcrFsException.java b/org.argeo.cms.jcr/src/org/argeo/jcr/fs/JcrFsException.java new file mode 100644 index 000000000..f214fdc44 --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/jcr/fs/JcrFsException.java @@ -0,0 +1,15 @@ +package org.argeo.jcr.fs; + + +/** Exception related to the JCR FS */ +public class JcrFsException extends RuntimeException { + private static final long serialVersionUID = -7973896038244922980L; + + public JcrFsException(String message, Throwable e) { + super(message, e); + } + + public JcrFsException(String message) { + super(message); + } +} diff --git a/org.argeo.cms.jcr/src/org/argeo/jcr/fs/JcrPath.java b/org.argeo.cms.jcr/src/org/argeo/jcr/fs/JcrPath.java new file mode 100644 index 000000000..1a4d74706 --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/jcr/fs/JcrPath.java @@ -0,0 +1,393 @@ +package org.argeo.jcr.fs; + +import java.io.File; +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.file.FileSystem; +import java.nio.file.LinkOption; +import java.nio.file.Path; +import java.nio.file.WatchEvent.Kind; +import java.nio.file.WatchEvent.Modifier; +import java.nio.file.WatchKey; +import java.nio.file.WatchService; +import java.util.Arrays; +import java.util.Iterator; +import java.util.NoSuchElementException; + +import javax.jcr.Node; +import javax.jcr.RepositoryException; + +/** A {@link Path} which contains a reference to a JCR {@link Node}. */ +public class JcrPath implements Path { + final static String separator = "/"; + final static char separatorChar = '/'; + + private final JcrFileSystem fs; + /** null for non absolute paths */ + private final WorkspaceFileStore fileStore; + private final String[] path;// null means root + private final boolean absolute; + + // optim + private final int hashCode; + + public JcrPath(JcrFileSystem filesSystem, String path) { + this.fs = filesSystem; + if (path == null) + throw new JcrFsException("Path cannot be null"); + if (path.equals(separator)) {// root + this.path = null; + this.absolute = true; + this.hashCode = 0; + this.fileStore = fs.getBaseFileStore(); + return; + } else if (path.equals("")) {// empty path + this.path = new String[] { "" }; + this.absolute = false; + this.fileStore = null; + this.hashCode = "".hashCode(); + return; + } + + if (path.equals("~")) {// home + path = filesSystem.getUserHomePath(); + if (path == null) + throw new JcrFsException("No home directory available"); + } + + this.absolute = path.charAt(0) == separatorChar ? true : false; + + this.fileStore = absolute ? fs.getFileStore(path) : null; + + String trimmedPath = path.substring(absolute ? 1 : 0, + path.charAt(path.length() - 1) == separatorChar ? path.length() - 1 : path.length()); + this.path = trimmedPath.split(separator); + for (int i = 0; i < this.path.length; i++) { + this.path[i] = Text.unescapeIllegalJcrChars(this.path[i]); + } + this.hashCode = this.path[this.path.length - 1].hashCode(); + assert !(absolute && fileStore == null); + } + + public JcrPath(JcrFileSystem filesSystem, Node node) throws RepositoryException { + this(filesSystem, filesSystem.getFileStore(node).toFsPath(node)); + } + + /** Internal optimisation */ + private JcrPath(JcrFileSystem filesSystem, WorkspaceFileStore fileStore, String[] path, boolean absolute) { + this.fs = filesSystem; + this.path = path; + this.absolute = path == null ? true : absolute; + if (this.absolute && fileStore == null) + throw new IllegalArgumentException("Absolute path requires a file store"); + if (!this.absolute && fileStore != null) + throw new IllegalArgumentException("A file store should not be provided for a relative path"); + this.fileStore = fileStore; + this.hashCode = path == null ? 0 : path[path.length - 1].hashCode(); + assert !(absolute && fileStore == null); + } + + @Override + public FileSystem getFileSystem() { + return fs; + } + + @Override + public boolean isAbsolute() { + return absolute; + } + + @Override + public Path getRoot() { + if (path == null) + return this; + return new JcrPath(fs, separator); + } + + @Override + public String toString() { + return toFsPath(path); + } + + private String toFsPath(String[] path) { + if (path == null) + return "/"; + StringBuilder sb = new StringBuilder(); + if (isAbsolute()) + sb.append('/'); + for (int i = 0; i < path.length; i++) { + if (i != 0) + sb.append('/'); + sb.append(path[i]); + } + return sb.toString(); + } + +// @Deprecated +// private String toJcrPath() { +// return toJcrPath(path); +// } +// +// @Deprecated +// private String toJcrPath(String[] path) { +// if (path == null) +// return "/"; +// StringBuilder sb = new StringBuilder(); +// if (isAbsolute()) +// sb.append('/'); +// for (int i = 0; i < path.length; i++) { +// if (i != 0) +// sb.append('/'); +// sb.append(Text.escapeIllegalJcrChars(path[i])); +// } +// return sb.toString(); +// } + + @Override + public Path getFileName() { + if (path == null) + return null; + return new JcrPath(fs, path[path.length - 1]); + } + + @Override + public Path getParent() { + if (path == null) + return null; + if (path.length == 1)// root + return new JcrPath(fs, separator); + String[] parentPath = Arrays.copyOfRange(path, 0, path.length - 1); + if (!absolute) + return new JcrPath(fs, null, parentPath, absolute); + else + return new JcrPath(fs, toFsPath(parentPath)); + } + + @Override + public int getNameCount() { + if (path == null) + return 0; + return path.length; + } + + @Override + public Path getName(int index) { + if (path == null) + return null; + return new JcrPath(fs, path[index]); + } + + @Override + public Path subpath(int beginIndex, int endIndex) { + if (path == null) + return null; + String[] parentPath = Arrays.copyOfRange(path, beginIndex, endIndex); + return new JcrPath(fs, null, parentPath, false); + } + + @Override + public boolean startsWith(Path other) { + return toString().startsWith(other.toString()); + } + + @Override + public boolean startsWith(String other) { + return toString().startsWith(other); + } + + @Override + public boolean endsWith(Path other) { + return toString().endsWith(other.toString()); + } + + @Override + public boolean endsWith(String other) { + return toString().endsWith(other); + } + + @Override + public Path normalize() { + // always normalized + return this; + } + + @Override + public Path resolve(Path other) { + JcrPath otherPath = (JcrPath) other; + if (otherPath.isAbsolute()) + return other; + String[] newPath; + if (path == null) { + newPath = new String[otherPath.path.length]; + System.arraycopy(otherPath.path, 0, newPath, 0, otherPath.path.length); + } else { + newPath = new String[path.length + otherPath.path.length]; + System.arraycopy(path, 0, newPath, 0, path.length); + System.arraycopy(otherPath.path, 0, newPath, path.length, otherPath.path.length); + } + if (!absolute) + return new JcrPath(fs, null, newPath, absolute); + else { + return new JcrPath(fs, toFsPath(newPath)); + } + } + + @Override + public final Path resolve(String other) { + return resolve(getFileSystem().getPath(other)); + } + + @Override + public final Path resolveSibling(Path other) { + if (other == null) + throw new NullPointerException(); + Path parent = getParent(); + return (parent == null) ? other : parent.resolve(other); + } + + @Override + public final Path resolveSibling(String other) { + return resolveSibling(getFileSystem().getPath(other)); + } + + @Override + public final Iterator iterator() { + return new Iterator() { + private int i = 0; + + @Override + public boolean hasNext() { + return (i < getNameCount()); + } + + @Override + public Path next() { + if (i < getNameCount()) { + Path result = getName(i); + i++; + return result; + } else { + throw new NoSuchElementException(); + } + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + }; + } + + @Override + public Path relativize(Path other) { + if (equals(other)) + return new JcrPath(fs, ""); + if (other.startsWith(this)) { + String p1 = toString(); + String p2 = other.toString(); + String relative = p2.substring(p1.length(), p2.length()); + if (relative.charAt(0) == '/') + relative = relative.substring(1); + return new JcrPath(fs, relative); + } + throw new IllegalArgumentException(other + " cannot be relativized against " + this); + } + + @Override + public URI toUri() { + try { + return new URI(fs.provider().getScheme(), toString(), null); + } catch (URISyntaxException e) { + throw new JcrFsException("Cannot create URI for " + toString(), e); + } + } + + @Override + public Path toAbsolutePath() { + if (isAbsolute()) + return this; + return new JcrPath(fs, fileStore, path, true); + } + + @Override + public Path toRealPath(LinkOption... options) throws IOException { + return this; + } + + @Override + public File toFile() { + throw new UnsupportedOperationException(); + } + + @Override + public WatchKey register(WatchService watcher, Kind[] events, Modifier... modifiers) throws IOException { + // TODO Auto-generated method stub + return null; + } + + @Override + public WatchKey register(WatchService watcher, Kind... events) throws IOException { + // TODO Auto-generated method stub + return null; + } + + @Override + public int compareTo(Path other) { + return toString().compareTo(other.toString()); + } + + public Node getNode() throws RepositoryException { + if (!isAbsolute())// TODO default dir + throw new JcrFsException("Cannot get a JCR node from a relative path"); + assert fileStore != null; + return fileStore.toNode(path); +// String pathStr = toJcrPath(); +// Session session = fs.getSession(); +// // TODO synchronize on the session ? +// if (!session.itemExists(pathStr)) +// return null; +// return session.getNode(pathStr); + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof JcrPath)) + return false; + JcrPath other = (JcrPath) obj; + + if (path == null) {// root + if (other.path == null)// root + return true; + else + return false; + } else { + if (other.path == null)// root + return false; + } + // non root + if (path.length != other.path.length) + return false; + for (int i = 0; i < path.length; i++) { + if (!path[i].equals(other.path[i])) + return false; + } + return true; + } + + @Override + public int hashCode() { + return hashCode; + } + + @Override + protected Object clone() throws CloneNotSupportedException { + return new JcrPath(fs, toString()); + } + + @Override + protected void finalize() throws Throwable { + Arrays.fill(path, null); + } + +} diff --git a/org.argeo.cms.jcr/src/org/argeo/jcr/fs/NodeDirectoryStream.java b/org.argeo.cms.jcr/src/org/argeo/jcr/fs/NodeDirectoryStream.java new file mode 100644 index 000000000..eda07a548 --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/jcr/fs/NodeDirectoryStream.java @@ -0,0 +1,77 @@ +package org.argeo.jcr.fs; + +import java.io.IOException; +import java.nio.file.DirectoryStream; +import java.nio.file.Path; +import java.util.Iterator; + +import javax.jcr.Node; +import javax.jcr.NodeIterator; + +public class NodeDirectoryStream implements DirectoryStream { + private final JcrFileSystem fs; + private final NodeIterator nodeIterator; + private final Iterator additionalPaths; + private final Filter filter; + + public NodeDirectoryStream(JcrFileSystem fs, NodeIterator nodeIterator, Iterator additionalPaths, + Filter filter) { + this.fs = fs; + this.nodeIterator = nodeIterator; + this.additionalPaths = additionalPaths; + this.filter = filter; + } + + @Override + public void close() throws IOException { + } + + @Override + public Iterator iterator() { + return new Iterator() { + private JcrPath next = null; + + @Override + public synchronized boolean hasNext() { + if (next != null) + return true; + nodes: while (nodeIterator.hasNext()) { + try { + Node node = nodeIterator.nextNode(); + String nodeName = node.getName(); + if (nodeName.startsWith("rep:") || nodeName.startsWith("jcr:")) + continue nodes; + if (fs.skipNode(node)) + continue nodes; + next = new JcrPath(fs, node); + if (filter != null) { + if (filter.accept(next)) + break nodes; + } else + break nodes; + } catch (Exception e) { + throw new JcrFsException("Could not get next path", e); + } + } + + if (next == null) { + if (additionalPaths.hasNext()) + next = additionalPaths.next(); + } + + return next != null; + } + + @Override + public synchronized Path next() { + if (!hasNext())// make sure has next has been called + return null; + JcrPath res = next; + next = null; + return res; + } + + }; + } + +} diff --git a/org.argeo.cms.jcr/src/org/argeo/jcr/fs/NodeFileAttributes.java b/org.argeo.cms.jcr/src/org/argeo/jcr/fs/NodeFileAttributes.java new file mode 100644 index 000000000..8054d52f8 --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/jcr/fs/NodeFileAttributes.java @@ -0,0 +1,9 @@ +package org.argeo.jcr.fs; + +import java.nio.file.attribute.BasicFileAttributes; + +import javax.jcr.Node; + +public interface NodeFileAttributes extends BasicFileAttributes { + public Node getNode(); +} diff --git a/org.argeo.cms.jcr/src/org/argeo/jcr/fs/Text.java b/org.argeo.cms.jcr/src/org/argeo/jcr/fs/Text.java new file mode 100644 index 000000000..4643c8c9c --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/jcr/fs/Text.java @@ -0,0 +1,877 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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. + */ +package org.argeo.jcr.fs; + +import java.io.ByteArrayOutputStream; +import java.io.UnsupportedEncodingException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; +import java.util.BitSet; +import java.util.Properties; + +/** + * Hacked from org.apache.jackrabbit.util.Text in Jackrabbit JCR Commons + * This Class provides some text related utilities + */ +class Text { + + /** + * Hidden constructor. + */ + private Text() { + } + + /** + * used for the md5 + */ + public static final char[] hexTable = "0123456789abcdef".toCharArray(); + + /** + * Calculate an MD5 hash of the string given. + * + * @param data + * the data to encode + * @param enc + * the character encoding to use + * @return a hex encoded string of the md5 digested input + */ + public static String md5(String data, String enc) throws UnsupportedEncodingException { + try { + return digest("MD5", data.getBytes(enc)); + } catch (NoSuchAlgorithmException e) { + throw new InternalError("MD5 digest not available???"); + } + } + + /** + * Calculate an MD5 hash of the string given using 'utf-8' encoding. + * + * @param data + * the data to encode + * @return a hex encoded string of the md5 digested input + */ + public static String md5(String data) { + try { + return md5(data, "utf-8"); + } catch (UnsupportedEncodingException e) { + throw new InternalError("UTF8 digest not available???"); + } + } + + /** + * Digest the plain string using the given algorithm. + * + * @param algorithm + * The alogrithm for the digest. This algorithm must be supported + * by the MessageDigest class. + * @param data + * The plain text String to be digested. + * @param enc + * The character encoding to use + * @return The digested plain text String represented as Hex digits. + * @throws java.security.NoSuchAlgorithmException + * if the desired algorithm is not supported by the + * MessageDigest class. + * @throws java.io.UnsupportedEncodingException + * if the encoding is not supported + */ + public static String digest(String algorithm, String data, String enc) + throws NoSuchAlgorithmException, UnsupportedEncodingException { + + return digest(algorithm, data.getBytes(enc)); + } + + /** + * Digest the plain string using the given algorithm. + * + * @param algorithm + * The algorithm for the digest. This algorithm must be supported + * by the MessageDigest class. + * @param data + * the data to digest with the given algorithm + * @return The digested plain text String represented as Hex digits. + * @throws java.security.NoSuchAlgorithmException + * if the desired algorithm is not supported by the + * MessageDigest class. + */ + public static String digest(String algorithm, byte[] data) throws NoSuchAlgorithmException { + + MessageDigest md = MessageDigest.getInstance(algorithm); + byte[] digest = md.digest(data); + StringBuilder res = new StringBuilder(digest.length * 2); + for (byte b : digest) { + res.append(hexTable[(b >> 4) & 15]); + res.append(hexTable[b & 15]); + } + return res.toString(); + } + + /** + * returns an array of strings decomposed of the original string, split at + * every occurrence of 'ch'. if 2 'ch' follow each other with no + * intermediate characters, empty "" entries are avoided. + * + * @param str + * the string to decompose + * @param ch + * the character to use a split pattern + * @return an array of strings + */ + public static String[] explode(String str, int ch) { + return explode(str, ch, false); + } + + /** + * returns an array of strings decomposed of the original string, split at + * every occurrence of 'ch'. + * + * @param str + * the string to decompose + * @param ch + * the character to use a split pattern + * @param respectEmpty + * if true, empty elements are generated + * @return an array of strings + */ + public static String[] explode(String str, int ch, boolean respectEmpty) { + if (str == null || str.length() == 0) { + return new String[0]; + } + + ArrayList strings = new ArrayList(); + int pos; + int lastpos = 0; + + // add snipples + while ((pos = str.indexOf(ch, lastpos)) >= 0) { + if (pos - lastpos > 0 || respectEmpty) { + strings.add(str.substring(lastpos, pos)); + } + lastpos = pos + 1; + } + // add rest + if (lastpos < str.length()) { + strings.add(str.substring(lastpos)); + } else if (respectEmpty && lastpos == str.length()) { + strings.add(""); + } + + // return string array + return strings.toArray(new String[strings.size()]); + } + + /** + * Concatenates all strings in the string array using the specified + * delimiter. + * + * @param arr + * @param delim + * @return the concatenated string + */ + public static String implode(String[] arr, String delim) { + StringBuilder buf = new StringBuilder(); + for (int i = 0; i < arr.length; i++) { + if (i > 0) { + buf.append(delim); + } + buf.append(arr[i]); + } + return buf.toString(); + } + + /** + * Replaces all occurrences of oldString in text + * with newString. + * + * @param text + * @param oldString + * old substring to be replaced with newString + * @param newString + * new substring to replace occurrences of oldString + * @return a string + */ + public static String replace(String text, String oldString, String newString) { + if (text == null || oldString == null || newString == null) { + throw new IllegalArgumentException("null argument"); + } + int pos = text.indexOf(oldString); + if (pos == -1) { + return text; + } + int lastPos = 0; + StringBuilder sb = new StringBuilder(text.length()); + while (pos != -1) { + sb.append(text.substring(lastPos, pos)); + sb.append(newString); + lastPos = pos + oldString.length(); + pos = text.indexOf(oldString, lastPos); + } + if (lastPos < text.length()) { + sb.append(text.substring(lastPos)); + } + return sb.toString(); + } + + /** + * Replaces XML characters in the given string that might need escaping as + * XML text or attribute + * + * @param text + * text to be escaped + * @return a string + */ + public static String encodeIllegalXMLCharacters(String text) { + return encodeMarkupCharacters(text, false); + } + + /** + * Replaces HTML characters in the given string that might need escaping as + * HTML text or attribute + * + * @param text + * text to be escaped + * @return a string + */ + public static String encodeIllegalHTMLCharacters(String text) { + return encodeMarkupCharacters(text, true); + } + + private static String encodeMarkupCharacters(String text, boolean isHtml) { + if (text == null) { + throw new IllegalArgumentException("null argument"); + } + StringBuilder buf = null; + int length = text.length(); + int pos = 0; + for (int i = 0; i < length; i++) { + int ch = text.charAt(i); + switch (ch) { + case '<': + case '>': + case '&': + case '"': + case '\'': + if (buf == null) { + buf = new StringBuilder(); + } + if (i > 0) { + buf.append(text.substring(pos, i)); + } + pos = i + 1; + break; + default: + continue; + } + if (ch == '<') { + buf.append("<"); + } else if (ch == '>') { + buf.append(">"); + } else if (ch == '&') { + buf.append("&"); + } else if (ch == '"') { + buf.append("""); + } else if (ch == '\'') { + buf.append(isHtml ? "'" : "'"); + } + } + if (buf == null) { + return text; + } else { + if (pos < length) { + buf.append(text.substring(pos)); + } + return buf.toString(); + } + } + + /** + * The list of characters that are not encoded by the escape() + * and unescape() METHODS. They contains the characters as + * defined 'unreserved' in section 2.3 of the RFC 2396 'URI generic syntax': + *

+ * + *

+	 * unreserved  = alphanum | mark
+	 * mark        = "-" | "_" | "." | "!" | "~" | "*" | "'" | "(" | ")"
+	 * 
+ */ + public static BitSet URISave; + + /** + * Same as {@link #URISave} but also contains the '/' + */ + public static BitSet URISaveEx; + + static { + URISave = new BitSet(256); + int i; + for (i = 'a'; i <= 'z'; i++) { + URISave.set(i); + } + for (i = 'A'; i <= 'Z'; i++) { + URISave.set(i); + } + for (i = '0'; i <= '9'; i++) { + URISave.set(i); + } + URISave.set('-'); + URISave.set('_'); + URISave.set('.'); + URISave.set('!'); + URISave.set('~'); + URISave.set('*'); + URISave.set('\''); + URISave.set('('); + URISave.set(')'); + + URISaveEx = (BitSet) URISave.clone(); + URISaveEx.set('/'); + } + + /** + * Does an URL encoding of the string using the + * escape character. The characters that don't need encoding + * are those defined 'unreserved' in section 2.3 of the 'URI generic syntax' + * RFC 2396, but without the escape character. + * + * @param string + * the string to encode. + * @param escape + * the escape character. + * @return the escaped string + * @throws NullPointerException + * if string is null. + */ + public static String escape(String string, char escape) { + return escape(string, escape, false); + } + + /** + * Does an URL encoding of the string using the + * escape character. The characters that don't need encoding + * are those defined 'unreserved' in section 2.3 of the 'URI generic syntax' + * RFC 2396, but without the escape character. If isPath is + * true, additionally the slash '/' is ignored, too. + * + * @param string + * the string to encode. + * @param escape + * the escape character. + * @param isPath + * if true, the string is treated as path + * @return the escaped string + * @throws NullPointerException + * if string is null. + */ + public static String escape(String string, char escape, boolean isPath) { + try { + BitSet validChars = isPath ? URISaveEx : URISave; + byte[] bytes = string.getBytes("utf-8"); + StringBuilder out = new StringBuilder(bytes.length); + for (byte aByte : bytes) { + int c = aByte & 0xff; + if (validChars.get(c) && c != escape) { + out.append((char) c); + } else { + out.append(escape); + out.append(hexTable[(c >> 4) & 0x0f]); + out.append(hexTable[(c) & 0x0f]); + } + } + return out.toString(); + } catch (UnsupportedEncodingException e) { + throw new InternalError(e.toString()); + } + } + + /** + * Does a URL encoding of the string. The characters that don't + * need encoding are those defined 'unreserved' in section 2.3 of the 'URI + * generic syntax' RFC 2396. + * + * @param string + * the string to encode + * @return the escaped string + * @throws NullPointerException + * if string is null. + */ + public static String escape(String string) { + return escape(string, '%'); + } + + /** + * Does a URL encoding of the path. The characters that don't + * need encoding are those defined 'unreserved' in section 2.3 of the 'URI + * generic syntax' RFC 2396. In contrast to the {@link #escape(String)} + * method, not the entire path string is escaped, but every individual part + * (i.e. the slashes are not escaped). + * + * @param path + * the path to encode + * @return the escaped path + * @throws NullPointerException + * if path is null. + */ + public static String escapePath(String path) { + return escape(path, '%', true); + } + + /** + * Does a URL decoding of the string using the + * escape character. Please note that in opposite to the + * {@link java.net.URLDecoder} it does not transform the + into spaces. + * + * @param string + * the string to decode + * @param escape + * the escape character + * @return the decoded string + * @throws NullPointerException + * if string is null. + * @throws IllegalArgumentException + * if the 2 characters following the escape character do not + * represent a hex-number or if not enough characters follow an + * escape character + */ + public static String unescape(String string, char escape) { + try { + byte[] utf8 = string.getBytes("utf-8"); + + // Check whether escape occurs at invalid position + if ((utf8.length >= 1 && utf8[utf8.length - 1] == escape) + || (utf8.length >= 2 && utf8[utf8.length - 2] == escape)) { + throw new IllegalArgumentException("Premature end of escape sequence at end of input"); + } + + ByteArrayOutputStream out = new ByteArrayOutputStream(utf8.length); + for (int k = 0; k < utf8.length; k++) { + byte b = utf8[k]; + if (b == escape) { + out.write((decodeDigit(utf8[++k]) << 4) + decodeDigit(utf8[++k])); + } else { + out.write(b); + } + } + + return new String(out.toByteArray(), "utf-8"); + } catch (UnsupportedEncodingException e) { + throw new InternalError(e.toString()); + } + } + + /** + * Does a URL decoding of the string. Please note that in + * opposite to the {@link java.net.URLDecoder} it does not transform the + + * into spaces. + * + * @param string + * the string to decode + * @return the decoded string + * @throws NullPointerException + * if string is null. + * @throws ArrayIndexOutOfBoundsException + * if not enough character follow an escape character + * @throws IllegalArgumentException + * if the 2 characters following the escape character do not + * represent a hex-number. + */ + public static String unescape(String string) { + return unescape(string, '%'); + } + + /** + * Escapes all illegal JCR name characters of a string. The encoding is + * loosely modeled after URI encoding, but only encodes the characters it + * absolutely needs to in order to make the resulting string a valid JCR + * name. Use {@link #unescapeIllegalJcrChars(String)} for decoding. + *

+ * QName EBNF:
+ *

simplename ::= onecharsimplename | twocharsimplename | + * threeormorecharname onecharsimplename ::= (* Any Unicode character + * except: '.', '/', ':', '[', ']', '*', '|' or any whitespace character *) + * twocharsimplename ::= '.' onecharsimplename | onecharsimplename '.' | + * onecharsimplename onecharsimplename threeormorecharname ::= nonspace + * string nonspace string ::= char | string char char ::= nonspace | ' ' + * nonspace ::= (* Any Unicode character except: '/', ':', '[', ']', '*', + * '|' or any whitespace character *) + * + * @param name + * the name to escape + * @return the escaped name + */ + public static String escapeIllegalJcrChars(String name) { + return escapeIllegalChars(name, "%/:[]*|\t\r\n"); + } + + /** + * Escapes all illegal JCR 1.0 name characters of a string. Use + * {@link #unescapeIllegalJcrChars(String)} for decoding. + *

+ * QName EBNF:
+ *

simplename ::= onecharsimplename | twocharsimplename | + * threeormorecharname onecharsimplename ::= (* Any Unicode character + * except: '.', '/', ':', '[', ']', '*', ''', '"', '|' or any whitespace + * character *) twocharsimplename ::= '.' onecharsimplename | + * onecharsimplename '.' | onecharsimplename onecharsimplename + * threeormorecharname ::= nonspace string nonspace string ::= char | string + * char char ::= nonspace | ' ' nonspace ::= (* Any Unicode character + * except: '/', ':', '[', ']', '*', ''', '"', '|' or any whitespace + * character *) + * + * @since Apache Jackrabbit 2.3.2 and 2.2.10 + * @see
JCR-3128 + * @param name + * the name to escape + * @return the escaped name + */ + public static String escapeIllegalJcr10Chars(String name) { + return escapeIllegalChars(name, "%/:[]*'\"|\t\r\n"); + } + + private static String escapeIllegalChars(String name, String illegal) { + StringBuilder buffer = new StringBuilder(name.length() * 2); + for (int i = 0; i < name.length(); i++) { + char ch = name.charAt(i); + if (illegal.indexOf(ch) != -1 || (ch == '.' && name.length() < 3) + || (ch == ' ' && (i == 0 || i == name.length() - 1))) { + buffer.append('%'); + buffer.append(Character.toUpperCase(Character.forDigit(ch / 16, 16))); + buffer.append(Character.toUpperCase(Character.forDigit(ch % 16, 16))); + } else { + buffer.append(ch); + } + } + return buffer.toString(); + } + + /** + * Escapes illegal XPath search characters at the end of a string. + *

+ * Example:
+ * A search string like 'test?' will run into a ParseException documented in + * http://issues.apache.org/jira/browse/JCR-1248 + * + * @param s + * the string to encode + * @return the escaped string + */ + public static String escapeIllegalXpathSearchChars(String s) { + StringBuilder sb = new StringBuilder(); + sb.append(s.substring(0, (s.length() - 1))); + char c = s.charAt(s.length() - 1); + // NOTE: keep this in sync with _ESCAPED_CHAR below! + if (c == '!' || c == '(' || c == ':' || c == '^' || c == '[' || c == ']' || c == '{' || c == '}' || c == '?') { + sb.append('\\'); + } + sb.append(c); + return sb.toString(); + } + + /** + * Unescapes previously escaped jcr chars. + *

+ * Please note, that this does not exactly the same as the url related + * {@link #unescape(String)}, since it handles the byte-encoding + * differently. + * + * @param name + * the name to unescape + * @return the unescaped name + */ + public static String unescapeIllegalJcrChars(String name) { + StringBuilder buffer = new StringBuilder(name.length()); + int i = name.indexOf('%'); + while (i > -1 && i + 2 < name.length()) { + buffer.append(name.toCharArray(), 0, i); + int a = Character.digit(name.charAt(i + 1), 16); + int b = Character.digit(name.charAt(i + 2), 16); + if (a > -1 && b > -1) { + buffer.append((char) (a * 16 + b)); + name = name.substring(i + 3); + } else { + buffer.append('%'); + name = name.substring(i + 1); + } + i = name.indexOf('%'); + } + buffer.append(name); + return buffer.toString(); + } + + /** + * Returns the name part of the path. If the given path is already a name + * (i.e. contains no slashes) it is returned. + * + * @param path + * the path + * @return the name part or null if path is + * null. + */ + public static String getName(String path) { + return getName(path, '/'); + } + + /** + * Returns the name part of the path, delimited by the given + * delim. If the given path is already a name (i.e. contains no + * delim characters) it is returned. + * + * @param path + * the path + * @param delim + * the delimiter + * @return the name part or null if path is + * null. + */ + public static String getName(String path, char delim) { + return path == null ? null : path.substring(path.lastIndexOf(delim) + 1); + } + + /** + * Same as {@link #getName(String)} but adding the possibility to pass paths + * that end with a trailing '/' + * + * @see #getName(String) + */ + public static String getName(String path, boolean ignoreTrailingSlash) { + if (ignoreTrailingSlash && path != null && path.endsWith("/") && path.length() > 1) { + path = path.substring(0, path.length() - 1); + } + return getName(path); + } + + /** + * Returns the namespace prefix of the given qname. If the + * prefix is missing, an empty string is returned. Please note, that this + * method does not validate the name or prefix. + *

+ * the qname has the format: qname := [prefix ':'] local; + * + * @param qname + * a qualified name + * @return the prefix of the name or "". + * + * @see #getLocalName(String) + * + * @throws NullPointerException + * if qname is null + */ + public static String getNamespacePrefix(String qname) { + int pos = qname.indexOf(':'); + return pos >= 0 ? qname.substring(0, pos) : ""; + } + + /** + * Returns the local name of the given qname. Please note, that + * this method does not validate the name. + *

+ * the qname has the format: qname := [prefix ':'] local; + * + * @param qname + * a qualified name + * @return the localname + * + * @see #getNamespacePrefix(String) + * + * @throws NullPointerException + * if qname is null + */ + public static String getLocalName(String qname) { + int pos = qname.indexOf(':'); + return pos >= 0 ? qname.substring(pos + 1) : qname; + } + + /** + * Determines, if two paths denote hierarchical siblins. + * + * @param p1 + * first path + * @param p2 + * second path + * @return true if on same level, false otherwise + */ + public static boolean isSibling(String p1, String p2) { + int pos1 = p1.lastIndexOf('/'); + int pos2 = p2.lastIndexOf('/'); + return (pos1 == pos2 && pos1 >= 0 && p1.regionMatches(0, p2, 0, pos1)); + } + + /** + * Determines if the descendant path is hierarchical a + * descendant of path. + * + * @param path + * the current path + * @param descendant + * the potential descendant + * @return true if the descendant is a descendant; + * false otherwise. + */ + public static boolean isDescendant(String path, String descendant) { + String pattern = path.endsWith("/") ? path : path + "/"; + return !pattern.equals(descendant) && descendant.startsWith(pattern); + } + + /** + * Determines if the descendant path is hierarchical a + * descendant of path or equal to it. + * + * @param path + * the path to check + * @param descendant + * the potential descendant + * @return true if the descendant is a descendant + * or equal; false otherwise. + */ + public static boolean isDescendantOrEqual(String path, String descendant) { + if (path.equals(descendant)) { + return true; + } else { + String pattern = path.endsWith("/") ? path : path + "/"; + return descendant.startsWith(pattern); + } + } + + /** + * Returns the nth relative parent of the path, where n=level. + *

+ * Example:
+ * + * Text.getRelativeParent("/foo/bar/test", 1) == "/foo/bar" + * + * + * @param path + * the path of the page + * @param level + * the level of the parent + */ + public static String getRelativeParent(String path, int level) { + int idx = path.length(); + while (level > 0) { + idx = path.lastIndexOf('/', idx - 1); + if (idx < 0) { + return ""; + } + level--; + } + return (idx == 0) ? "/" : path.substring(0, idx); + } + + /** + * Same as {@link #getRelativeParent(String, int)} but adding the + * possibility to pass paths that end with a trailing '/' + * + * @see #getRelativeParent(String, int) + */ + public static String getRelativeParent(String path, int level, boolean ignoreTrailingSlash) { + if (ignoreTrailingSlash && path.endsWith("/") && path.length() > 1) { + path = path.substring(0, path.length() - 1); + } + return getRelativeParent(path, level); + } + + /** + * Returns the nth absolute parent of the path, where n=level. + *

+ * Example:
+ * + * Text.getAbsoluteParent("/foo/bar/test", 1) == "/foo/bar" + * + * + * @param path + * the path of the page + * @param level + * the level of the parent + */ + public static String getAbsoluteParent(String path, int level) { + int idx = 0; + int len = path.length(); + while (level >= 0 && idx < len) { + idx = path.indexOf('/', idx + 1); + if (idx < 0) { + idx = len; + } + level--; + } + return level >= 0 ? "" : path.substring(0, idx); + } + + /** + * Performs variable replacement on the given string value. Each + * ${...} sequence within the given value is replaced with the + * value of the named parser variable. If a variable is not found in the + * properties an IllegalArgumentException is thrown unless + * ignoreMissing is true. In the later case, the + * missing variable is replaced by the empty string. + * + * @param value + * the original value + * @param ignoreMissing + * if true, missing variables are replaced by the + * empty string. + * @return value after variable replacements + * @throws IllegalArgumentException + * if the replacement of a referenced variable is not found + */ + public static String replaceVariables(Properties variables, String value, boolean ignoreMissing) + throws IllegalArgumentException { + StringBuilder result = new StringBuilder(); + + // Value: + // +--+-+--------+-+-----------------+ + // | |p|--> |q|--> | + // +--+-+--------+-+-----------------+ + int p = 0, q = value.indexOf("${"); // Find first ${ + while (q != -1) { + result.append(value.substring(p, q)); // Text before ${ + p = q; + q = value.indexOf("}", q + 2); // Find } + if (q != -1) { + String variable = value.substring(p + 2, q); + String replacement = variables.getProperty(variable); + if (replacement == null) { + if (ignoreMissing) { + replacement = ""; + } else { + throw new IllegalArgumentException("Replacement not found for ${" + variable + "}."); + } + } + result.append(replacement); + p = q + 1; + q = value.indexOf("${", p); // Find next ${ + } + } + result.append(value.substring(p, value.length())); // Trailing text + + return result.toString(); + } + + private static byte decodeDigit(byte b) { + if (b >= 0x30 && b <= 0x39) { + return (byte) (b - 0x30); + } else if (b >= 0x41 && b <= 0x46) { + return (byte) (b - 0x37); + } else if (b >= 0x61 && b <= 0x66) { + return (byte) (b - 0x57); + } else { + throw new IllegalArgumentException("Escape sequence is not hexadecimal: " + (char) b); + } + } + +} diff --git a/org.argeo.cms.jcr/src/org/argeo/jcr/fs/WorkspaceFileStore.java b/org.argeo.cms.jcr/src/org/argeo/jcr/fs/WorkspaceFileStore.java new file mode 100644 index 000000000..6d9d05c2a --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/jcr/fs/WorkspaceFileStore.java @@ -0,0 +1,191 @@ +package org.argeo.jcr.fs; + +import java.io.IOException; +import java.nio.file.FileStore; +import java.nio.file.attribute.FileAttributeView; +import java.nio.file.attribute.FileStoreAttributeView; +import java.util.Arrays; + +import javax.jcr.Node; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.Workspace; + +import org.argeo.jcr.JcrUtils; + +/** A {@link FileStore} implementation based on JCR {@link Workspace}. */ +public class WorkspaceFileStore extends FileStore { + private final String mountPath; + private final Workspace workspace; + private final String workspaceName; + private final int mountDepth; + + public WorkspaceFileStore(String mountPath, Workspace workspace) { + if ("/".equals(mountPath) || "".equals(mountPath)) + throw new IllegalArgumentException( + "Mount path '" + mountPath + "' is unsupported, use null for the base file store"); + if (mountPath != null && !mountPath.startsWith(JcrPath.separator)) + throw new IllegalArgumentException("Mount path '" + mountPath + "' cannot end with /"); + if (mountPath != null && mountPath.endsWith(JcrPath.separator)) + throw new IllegalArgumentException("Mount path '" + mountPath + "' cannot end with /"); + this.mountPath = mountPath; + if (mountPath == null) + mountDepth = 0; + else { + mountDepth = mountPath.split(JcrPath.separator).length - 1; + } + this.workspace = workspace; + this.workspaceName = workspace.getName(); + } + + public void close() { + JcrUtils.logoutQuietly(workspace.getSession()); + } + + @Override + public String name() { + return workspace.getName(); + } + + @Override + public String type() { + return "workspace"; + } + + @Override + public boolean isReadOnly() { + return false; + } + + @Override + public long getTotalSpace() throws IOException { + return 0; + } + + @Override + public long getUsableSpace() throws IOException { + return 0; + } + + @Override + public long getUnallocatedSpace() throws IOException { + return 0; + } + + @Override + public boolean supportsFileAttributeView(Class type) { + return false; + } + + @Override + public boolean supportsFileAttributeView(String name) { + return false; + } + + @Override + public V getFileStoreAttributeView(Class type) { + return null; + } + + @Override + public Object getAttribute(String attribute) throws IOException { + return workspace.getSession().getRepository().getDescriptor(attribute); + } + + public Workspace getWorkspace() { + return workspace; + } + + public String toFsPath(Node node) throws RepositoryException { + String nodeWorkspaceName = node.getSession().getWorkspace().getName(); + if (!nodeWorkspaceName.equals(workspace.getName())) + throw new IllegalArgumentException("Icompatible " + node + " from workspace '" + nodeWorkspaceName + + "' in file store '" + workspace.getName() + "'"); + return mountPath == null ? node.getPath() : mountPath + node.getPath(); + } + + public boolean isBase() { + return mountPath == null; + } + + Node toNode(String[] fullPath) throws RepositoryException { + String jcrPath = toJcrPath(fullPath); + Session session = workspace.getSession(); + if (!session.itemExists(jcrPath)) + return null; + Node node = session.getNode(jcrPath); + return node; + } + + String toJcrPath(String fsPath) { + if (fsPath.length() == 1) + return toJcrPath((String[]) null);// root + String[] arr = fsPath.substring(1).split("/"); +// if (arr.length == 0 || (arr.length == 1 && arr[0].equals(""))) +// return toJcrPath((String[]) null);// root +// else + return toJcrPath(arr); + } + + private String toJcrPath(String[] path) { + if (path == null) + return "/"; + if (path.length < mountDepth) + throw new IllegalArgumentException( + "Path " + Arrays.asList(path) + " is no compatible with mount " + mountPath); + + if (!isBase()) { + // check mount compatibility + StringBuilder mount = new StringBuilder(); + mount.append('/'); + for (int i = 0; i < mountDepth; i++) { + if (i != 0) + mount.append('/'); + mount.append(Text.escapeIllegalJcrChars(path[i])); + } + if (!mountPath.equals(mount.toString())) + throw new IllegalArgumentException( + "Path " + Arrays.asList(path) + " is no compatible with mount " + mountPath); + } + + StringBuilder sb = new StringBuilder(); + sb.append('/'); + for (int i = mountDepth; i < path.length; i++) { + if (i != mountDepth) + sb.append('/'); + sb.append(Text.escapeIllegalJcrChars(path[i])); + } + return sb.toString(); + } + + public String getMountPath() { + return mountPath; + } + + public String getWorkspaceName() { + return workspaceName; + } + + public int getMountDepth() { + return mountDepth; + } + + @Override + public int hashCode() { + return workspaceName.hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof WorkspaceFileStore)) + return false; + WorkspaceFileStore other = (WorkspaceFileStore) obj; + return workspaceName.equals(other.workspaceName); + } + + @Override + public String toString() { + return "WorkspaceFileStore " + workspaceName; + } + +} diff --git a/org.argeo.cms.jcr/src/org/argeo/jcr/fs/package-info.java b/org.argeo.cms.jcr/src/org/argeo/jcr/fs/package-info.java new file mode 100644 index 000000000..0cdfdaf43 --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/jcr/fs/package-info.java @@ -0,0 +1,2 @@ +/** Java NIO file system implementation based on plain JCR. */ +package org.argeo.jcr.fs; \ No newline at end of file diff --git a/org.argeo.cms.jcr/src/org/argeo/jcr/jcrx.cnd b/org.argeo.cms.jcr/src/org/argeo/jcr/jcrx.cnd new file mode 100644 index 000000000..3eb0e7a3d --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/jcr/jcrx.cnd @@ -0,0 +1,16 @@ +// +// JCR EXTENSIONS +// + + +[jcrx:xmlvalue] +- * ++ jcr:xmltext (jcrx:xmltext) = jcrx:xmltext + +[jcrx:xmltext] + - jcr:xmlcharacters (STRING) mandatory + +[jcrx:csum] +mixin + - jcrx:sum (STRING) * + \ No newline at end of file diff --git a/org.argeo.cms.jcr/src/org/argeo/jcr/package-info.java b/org.argeo.cms.jcr/src/org/argeo/jcr/package-info.java new file mode 100644 index 000000000..1837749f1 --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/jcr/package-info.java @@ -0,0 +1,2 @@ +/** Generic JCR utilities. */ +package org.argeo.jcr; \ No newline at end of file diff --git a/org.argeo.cms.jcr/src/org/argeo/jcr/proxy/AbstractUrlProxy.java b/org.argeo.cms.jcr/src/org/argeo/jcr/proxy/AbstractUrlProxy.java new file mode 100644 index 000000000..0984276dd --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/jcr/proxy/AbstractUrlProxy.java @@ -0,0 +1,154 @@ +package org.argeo.jcr.proxy; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; + +import javax.jcr.Binary; +import javax.jcr.Node; +import javax.jcr.Property; +import javax.jcr.Repository; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.nodetype.NodeType; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.argeo.jcr.JcrException; +import org.argeo.jcr.JcrUtils; + +/** Base class for URL based proxys. */ +public abstract class AbstractUrlProxy implements ResourceProxy { + private final static Log log = LogFactory.getLog(AbstractUrlProxy.class); + + private Repository jcrRepository; + private Session jcrAdminSession; + private String proxyWorkspace = "proxy"; + + protected abstract Node retrieve(Session session, String path); + + void init() { + try { + jcrAdminSession = JcrUtils.loginOrCreateWorkspace(jcrRepository, proxyWorkspace); + beforeInitSessionSave(jcrAdminSession); + if (jcrAdminSession.hasPendingChanges()) + jcrAdminSession.save(); + } catch (RepositoryException e) { + JcrUtils.discardQuietly(jcrAdminSession); + throw new JcrException("Cannot initialize URL proxy", e); + } + } + + /** + * Called before the (admin) session is saved at the end of the initialization. + * Does nothing by default, to be overridden. + */ + protected void beforeInitSessionSave(Session session) throws RepositoryException { + } + + void destroy() { + JcrUtils.logoutQuietly(jcrAdminSession); + } + + /** + * Called before the (admin) session is logged out when resources are released. + * Does nothing by default, to be overridden. + */ + protected void beforeDestroySessionLogout() throws RepositoryException { + } + + public Node proxy(String path) { + // we open a JCR session with client credentials in order not to use the + // admin session in multiple thread or make it a bottleneck. + Node nodeAdmin = null; + Node nodeClient = null; + Session clientSession = null; + try { + clientSession = jcrRepository.login(proxyWorkspace); + if (!clientSession.itemExists(path) || shouldUpdate(clientSession, path)) { + nodeAdmin = retrieveAndSave(path); + if (nodeAdmin != null) + nodeClient = clientSession.getNode(path); + } else + nodeClient = clientSession.getNode(path); + return nodeClient; + } catch (RepositoryException e) { + throw new JcrException("Cannot proxy " + path, e); + } finally { + if (nodeClient == null) + JcrUtils.logoutQuietly(clientSession); + } + } + + protected synchronized Node retrieveAndSave(String path) { + try { + Node node = retrieve(jcrAdminSession, path); + if (node == null) + return null; + jcrAdminSession.save(); + return node; + } catch (RepositoryException e) { + JcrUtils.discardQuietly(jcrAdminSession); + throw new JcrException("Cannot retrieve and save " + path, e); + } finally { + notifyAll(); + } + } + + /** Session is not saved */ + protected synchronized Node proxyUrl(Session session, String remoteUrl, String path) throws RepositoryException { + Node node = null; + if (session.itemExists(path)) { + // throw new ArgeoJcrException("Node " + path + " already exists"); + } + try (InputStream in = new URL(remoteUrl).openStream()) { + // URL u = new URL(remoteUrl); + // in = u.openStream(); + node = importFile(session, path, in); + } catch (IOException e) { + if (log.isDebugEnabled()) { + log.debug("Cannot read " + remoteUrl + ", skipping... " + e.getMessage()); + // log.trace("Cannot read because of ", e); + } + JcrUtils.discardQuietly(session); + // } finally { + // IOUtils.closeQuietly(in); + } + return node; + } + + protected synchronized Node importFile(Session session, String path, InputStream in) throws RepositoryException { + Binary binary = null; + try { + Node content = null; + Node node = null; + if (!session.itemExists(path)) { + node = JcrUtils.mkdirs(session, path, NodeType.NT_FILE, NodeType.NT_FOLDER, false); + content = node.addNode(Node.JCR_CONTENT, NodeType.NT_UNSTRUCTURED); + } else { + node = session.getNode(path); + content = node.getNode(Node.JCR_CONTENT); + } + binary = session.getValueFactory().createBinary(in); + content.setProperty(Property.JCR_DATA, binary); + JcrUtils.updateLastModifiedAndParents(node, null, true); + return node; + } finally { + JcrUtils.closeQuietly(binary); + } + } + + /** Whether the file should be updated. */ + protected Boolean shouldUpdate(Session clientSession, String nodePath) { + return false; + } + + public void setJcrRepository(Repository jcrRepository) { + this.jcrRepository = jcrRepository; + } + + public void setProxyWorkspace(String localWorkspace) { + this.proxyWorkspace = localWorkspace; + } + +} diff --git a/org.argeo.cms.jcr/src/org/argeo/jcr/proxy/ResourceProxy.java b/org.argeo.cms.jcr/src/org/argeo/jcr/proxy/ResourceProxy.java new file mode 100644 index 000000000..84eea1f31 --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/jcr/proxy/ResourceProxy.java @@ -0,0 +1,16 @@ +package org.argeo.jcr.proxy; + +import javax.jcr.Node; + +/** A proxy which nows how to resolve and synchronize relative URLs */ +public interface ResourceProxy { + /** + * Proxy the file referenced by this relative path in the underlying + * repository. A new session is created by each call, so the underlying + * session of the returned node must be closed by the caller. + * + * @return the proxied Node, null if the resource was not found + * (e.g. HTTP 404) + */ + public Node proxy(String relativePath); +} diff --git a/org.argeo.cms.jcr/src/org/argeo/jcr/proxy/ResourceProxyServlet.java b/org.argeo.cms.jcr/src/org/argeo/jcr/proxy/ResourceProxyServlet.java new file mode 100644 index 000000000..d77bd49dc --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/jcr/proxy/ResourceProxyServlet.java @@ -0,0 +1,116 @@ +package org.argeo.jcr.proxy; + +import java.io.IOException; +import java.io.InputStream; + +import javax.jcr.Node; +import javax.jcr.Property; +import javax.jcr.RepositoryException; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.apache.commons.io.FilenameUtils; +import org.apache.commons.io.IOUtils; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.argeo.jcr.JcrException; +import org.argeo.jcr.Bin; +import org.argeo.jcr.JcrUtils; + +/** Wraps a proxy via HTTP */ +public class ResourceProxyServlet extends HttpServlet { + private static final long serialVersionUID = -8886549549223155801L; + + private final static Log log = LogFactory + .getLog(ResourceProxyServlet.class); + + private ResourceProxy proxy; + + private String contentTypeCharset = "UTF-8"; + + @Override + protected void doGet(HttpServletRequest request, + HttpServletResponse response) throws ServletException, IOException { + String path = request.getPathInfo(); + + if (log.isTraceEnabled()) { + log.trace("path=" + path); + log.trace("UserPrincipal = " + request.getUserPrincipal().getName()); + log.trace("SessionID = " + request.getSession(false).getId()); + log.trace("ContextPath = " + request.getContextPath()); + log.trace("ServletPath = " + request.getServletPath()); + log.trace("PathInfo = " + request.getPathInfo()); + log.trace("Method = " + request.getMethod()); + log.trace("User-Agent = " + request.getHeader("User-Agent")); + } + + Node node = null; + try { + node = proxy.proxy(path); + if (node == null) + response.sendError(404); + else + processResponse(node, response); + } finally { + if (node != null) + try { + JcrUtils.logoutQuietly(node.getSession()); + } catch (RepositoryException e) { + // silent + } + } + + } + + /** Retrieve the content of the node. */ + protected void processResponse(Node node, HttpServletResponse response) { +// Binary binary = null; +// InputStream in = null; + try(Bin binary = new Bin( node.getNode(Property.JCR_CONTENT) + .getProperty(Property.JCR_DATA));InputStream in = binary.getStream()) { + String fileName = node.getName(); + String ext = FilenameUtils.getExtension(fileName); + + // TODO use a more generic / standard approach + // see http://svn.apache.org/viewvc/tomcat/trunk/conf/web.xml + String contentType; + if ("xml".equals(ext)) + contentType = "text/xml;charset=" + contentTypeCharset; + else if ("jar".equals(ext)) + contentType = "application/java-archive"; + else if ("zip".equals(ext)) + contentType = "application/zip"; + else if ("gz".equals(ext)) + contentType = "application/x-gzip"; + else if ("bz2".equals(ext)) + contentType = "application/x-bzip2"; + else if ("tar".equals(ext)) + contentType = "application/x-tar"; + else if ("rpm".equals(ext)) + contentType = "application/x-redhat-package-manager"; + else + contentType = "application/octet-stream"; + contentType = contentType + ";name=\"" + fileName + "\""; + response.setHeader("Content-Disposition", "attachment; filename=\"" + + fileName + "\""); + response.setHeader("Expires", "0"); + response.setHeader("Cache-Control", "no-cache, must-revalidate"); + response.setHeader("Pragma", "no-cache"); + + response.setContentType(contentType); + + IOUtils.copy(in, response.getOutputStream()); + } catch (RepositoryException e) { + throw new JcrException("Cannot download " + node, e); + } catch (IOException e) { + throw new RuntimeException("Cannot download " + node, e); + } + } + + public void setProxy(ResourceProxy resourceProxy) { + this.proxy = resourceProxy; + } + +} diff --git a/org.argeo.cms.jcr/src/org/argeo/jcr/proxy/package-info.java b/org.argeo.cms.jcr/src/org/argeo/jcr/proxy/package-info.java new file mode 100644 index 000000000..a578c456e --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/jcr/proxy/package-info.java @@ -0,0 +1,2 @@ +/** Components to build proxys based on JCR. */ +package org.argeo.jcr.proxy; \ No newline at end of file diff --git a/org.argeo.cms.jcr/src/org/argeo/jcr/unit/AbstractJcrTestCase.java b/org.argeo.cms.jcr/src/org/argeo/jcr/unit/AbstractJcrTestCase.java new file mode 100644 index 000000000..dc2963a51 --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/jcr/unit/AbstractJcrTestCase.java @@ -0,0 +1,116 @@ +package org.argeo.jcr.unit; + +import java.io.File; +import java.security.AccessController; +import java.security.PrivilegedAction; + +import javax.jcr.Repository; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.SimpleCredentials; +import javax.security.auth.Subject; +import javax.security.auth.login.LoginContext; +import javax.security.auth.login.LoginException; + +import org.apache.commons.io.FileUtils; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.argeo.jcr.JcrException; + +import junit.framework.TestCase; + +/** Base for unit tests with a JCR repository. */ +public abstract class AbstractJcrTestCase extends TestCase { + private final static Log log = LogFactory.getLog(AbstractJcrTestCase.class); + + private Repository repository; + private Session session = null; + + public final static String LOGIN_CONTEXT_TEST_SYSTEM = "TEST_JACKRABBIT_ADMIN"; + + // protected abstract File getRepositoryFile() throws Exception; + + protected abstract Repository createRepository() throws Exception; + + protected abstract void clearRepository(Repository repository) throws Exception; + + @Override + protected void setUp() throws Exception { + File homeDir = getHomeDir(); + FileUtils.deleteDirectory(homeDir); + repository = createRepository(); + } + + @Override + protected void tearDown() throws Exception { + if (session != null) { + session.logout(); + if (log.isTraceEnabled()) + log.trace("Logout session"); + } + clearRepository(repository); + } + + protected Session session() { + if (session != null && session.isLive()) + return session; + Session session; + if (getLoginContext() != null) { + LoginContext lc; + try { + lc = new LoginContext(getLoginContext()); + lc.login(); + } catch (LoginException e) { + throw new IllegalStateException("JAAS login failed", e); + } + session = Subject.doAs(lc.getSubject(), new PrivilegedAction() { + + @Override + public Session run() { + return login(); + } + + }); + } else + session = login(); + this.session = session; + return this.session; + } + + protected String getLoginContext() { + return null; + } + + protected Session login() { + try { + if (log.isTraceEnabled()) + log.trace("Login session"); + Subject subject = Subject.getSubject(AccessController.getContext()); + if (subject != null) + return getRepository().login(); + else + return getRepository().login(new SimpleCredentials("demo", "demo".toCharArray())); + } catch (RepositoryException e) { + throw new JcrException("Cannot login to repository", e); + } + } + + protected Repository getRepository() { + return repository; + } + + /** + * enables children class to set an existing repository in case it is not + * deleted on startup, to test migration by instance + */ + public void setRepository(Repository repository) { + this.repository = repository; + } + + protected File getHomeDir() { + File homeDir = new File(System.getProperty("java.io.tmpdir"), + AbstractJcrTestCase.class.getSimpleName() + "-" + System.getProperty("user.name")); + return homeDir; + } + +} diff --git a/org.argeo.cms.jcr/src/org/argeo/jcr/unit/package-info.java b/org.argeo.cms.jcr/src/org/argeo/jcr/unit/package-info.java new file mode 100644 index 000000000..c6e741524 --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/jcr/unit/package-info.java @@ -0,0 +1,2 @@ +/** Helpers for unit tests with JCR repositories. */ +package org.argeo.jcr.unit; \ No newline at end of file diff --git a/org.argeo.cms.jcr/src/org/argeo/jcr/xml/JcrXmlUtils.java b/org.argeo.cms.jcr/src/org/argeo/jcr/xml/JcrXmlUtils.java new file mode 100644 index 000000000..2adb6a97e --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/jcr/xml/JcrXmlUtils.java @@ -0,0 +1,186 @@ +package org.argeo.jcr.xml; + +import java.io.IOException; +import java.io.Writer; +import java.util.Map; +import java.util.TreeMap; + +import javax.jcr.Node; +import javax.jcr.NodeIterator; +import javax.jcr.Property; +import javax.jcr.PropertyIterator; +import javax.jcr.RepositoryException; +import javax.jcr.Value; +import javax.jcr.nodetype.NodeType; + +import org.argeo.jcr.Jcr; + +/** Utilities around JCR and XML. */ +public class JcrXmlUtils { + /** + * Convenience method calling {@link #toXmlElements(Writer, Node, boolean)} with + * false. + */ + public static void toXmlElements(Writer writer, Node node) throws RepositoryException, IOException { + toXmlElements(writer, node, null, false, false, false); + } + + /** + * Write JCR properties as XML elements in a tree structure whose elements are + * named by node primary type. + * + * @param writer the writer to use + * @param node the subtree + * @param depth maximal depth, or if null the whole + * subtree. It must be positive, with depth 0 + * describing just the node without its children. + * @param withMetadata whether to write the primary type and mixins as + * elements + * @param withPrefix whether to keep the namespace prefixes + * @param propertiesAsElements whether single properties should be written as + * elements rather than attributes. If + * false, multiple properties will be + * skipped. + */ + public static void toXmlElements(Writer writer, Node node, Integer depth, boolean withMetadata, boolean withPrefix, + boolean propertiesAsElements) throws RepositoryException, IOException { + if (depth != null && depth < 0) + throw new IllegalArgumentException("Depth " + depth + " is negative."); + + if (node.getName().equals(Jcr.JCR_XMLTEXT)) { + writer.write(node.getProperty(Jcr.JCR_XMLCHARACTERS).getString()); + return; + } + + if (!propertiesAsElements) { + Map attrs = new TreeMap<>(); + PropertyIterator pit = node.getProperties(); + properties: while (pit.hasNext()) { + Property p = pit.nextProperty(); + if (!p.isMultiple()) { + String pName = p.getName(); + if (!withMetadata && (pName.equals(Jcr.JCR_PRIMARY_TYPE) || pName.equals(Jcr.JCR_UUID) + || pName.equals(Jcr.JCR_CREATED) || pName.equals(Jcr.JCR_CREATED_BY) + || pName.equals(Jcr.JCR_LAST_MODIFIED) || pName.equals(Jcr.JCR_LAST_MODIFIED_BY))) + continue properties; + attrs.put(withPrefix(p.getName(), withPrefix), p.getString()); + } + } + if (withMetadata && node.hasProperty(Property.JCR_UUID)) + attrs.put("id", "urn:uuid:" + node.getProperty(Property.JCR_UUID).getString()); + attrs.put(withPrefix ? Jcr.JCR_NAME : "name", node.getName()); + writeStart(writer, withPrefix(node.getPrimaryNodeType().getName(), withPrefix), attrs, node.hasNodes()); + } else { + if (withMetadata && node.hasProperty(Property.JCR_UUID)) { + writeStart(writer, withPrefix(node.getPrimaryNodeType().getName(), withPrefix), "id", + "urn:uuid:" + node.getProperty(Property.JCR_UUID).getString()); + } else { + writeStart(writer, withPrefix(node.getPrimaryNodeType().getName(), withPrefix)); + } + // name + writeStart(writer, withPrefix ? Jcr.JCR_NAME : "name"); + writer.append(node.getName()); + writeEnd(writer, withPrefix ? Jcr.JCR_NAME : "name"); + } + + // mixins + if (withMetadata) { + for (NodeType mixin : node.getMixinNodeTypes()) { + writeStart(writer, withPrefix ? Jcr.JCR_MIXIN_TYPES : "mixinTypes"); + writer.append(mixin.getName()); + writeEnd(writer, withPrefix ? Jcr.JCR_MIXIN_TYPES : "mixinTypes"); + } + } + + // properties as elements + if (propertiesAsElements) { + PropertyIterator pit = node.getProperties(); + properties: while (pit.hasNext()) { + Property p = pit.nextProperty(); + if (p.isMultiple()) { + for (Value value : p.getValues()) { + writeStart(writer, withPrefix(p.getName(), withPrefix)); + writer.write(value.getString()); + writeEnd(writer, withPrefix(p.getName(), withPrefix)); + } + } else { + Value value = p.getValue(); + String pName = p.getName(); + if (!withMetadata && (pName.equals(Jcr.JCR_PRIMARY_TYPE) || pName.equals(Jcr.JCR_UUID) + || pName.equals(Jcr.JCR_CREATED) || pName.equals(Jcr.JCR_CREATED_BY) + || pName.equals(Jcr.JCR_LAST_MODIFIED) || pName.equals(Jcr.JCR_LAST_MODIFIED_BY))) + continue properties; + writeStart(writer, withPrefix(p.getName(), withPrefix)); + writer.write(value.getString()); + writeEnd(writer, withPrefix(p.getName(), withPrefix)); + } + } + } + + // children + if (node.hasNodes()) { + if (depth == null || depth > 0) { + NodeIterator nit = node.getNodes(); + while (nit.hasNext()) { + toXmlElements(writer, nit.nextNode(), depth == null ? null : depth - 1, withMetadata, withPrefix, + propertiesAsElements); + } + } + writeEnd(writer, withPrefix(node.getPrimaryNodeType().getName(), withPrefix)); + } + } + + private static String withPrefix(String str, boolean withPrefix) { + if (withPrefix) + return str; + int index = str.indexOf(':'); + if (index < 0) + return str; + return str.substring(index + 1); + } + + private static void writeStart(Writer writer, String tagName) throws IOException { + writer.append('<'); + writer.append(tagName); + writer.append('>'); + } + + private static void writeStart(Writer writer, String tagName, String attr, String value) throws IOException { + writer.append('<'); + writer.append(tagName); + writer.append(' '); + writer.append(attr); + writer.append("=\""); + writer.append(value); + writer.append("\">"); + } + + private static void writeStart(Writer writer, String tagName, Map attrs, boolean hasChildren) + throws IOException { + writer.append('<'); + writer.append(tagName); + for (String attr : attrs.keySet()) { + writer.append(' '); + writer.append(attr); + writer.append("=\""); + writer.append(attrs.get(attr)); + writer.append('\"'); + } + if (hasChildren) + writer.append('>'); + else + writer.append("/>"); + } + + private static void writeEnd(Writer writer, String tagName) throws IOException { + writer.append("'); + } + + /** Singleton. */ + private JcrXmlUtils() { + + } + +} diff --git a/org.argeo.cms.jcr/src/org/argeo/jcr/xml/removePrefixes.xsl b/org.argeo.cms.jcr/src/org/argeo/jcr/xml/removePrefixes.xsl new file mode 100644 index 000000000..813d06570 --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/jcr/xml/removePrefixes.xsl @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/org.argeo.cms.jcr/src/org/argeo/maintenance/AbstractMaintenanceService.java b/org.argeo.cms.jcr/src/org/argeo/maintenance/AbstractMaintenanceService.java new file mode 100644 index 000000000..6003d638d --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/maintenance/AbstractMaintenanceService.java @@ -0,0 +1,221 @@ +package org.argeo.maintenance; + +import java.io.IOException; +import java.util.EnumSet; +import java.util.HashSet; +import java.util.Set; + +import javax.jcr.NoSuchWorkspaceException; +import javax.jcr.Repository; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.transaction.UserTransaction; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.argeo.api.NodeUtils; +import org.argeo.jcr.Jcr; +import org.argeo.jcr.JcrUtils; +import org.argeo.naming.Distinguished; +import org.osgi.service.useradmin.Group; +import org.osgi.service.useradmin.Role; +import org.osgi.service.useradmin.UserAdmin; + +/** Make sure roles and access rights are properly configured. */ +public abstract class AbstractMaintenanceService { + private final static Log log = LogFactory.getLog(AbstractMaintenanceService.class); + + private Repository repository; +// private UserAdminService userAdminService; + private UserAdmin userAdmin; + private UserTransaction userTransaction; + + public void init() { + makeSureRolesExists(getRequiredRoles()); + configureStandardRoles(); + + Set workspaceNames = getWorkspaceNames(); + if (workspaceNames == null || workspaceNames.isEmpty()) { + configureJcr(repository, null); + } else { + for (String workspaceName : workspaceNames) + configureJcr(repository, workspaceName); + } + } + + /** Configures a workspace. */ + protected void configureJcr(Repository repository, String workspaceName) { + Session adminSession; + try { + adminSession = NodeUtils.openDataAdminSession(repository, workspaceName); + } catch (RuntimeException e1) { + if (e1.getCause() != null && e1.getCause() instanceof NoSuchWorkspaceException) { + Session defaultAdminSession = NodeUtils.openDataAdminSession(repository, null); + try { + defaultAdminSession.getWorkspace().createWorkspace(workspaceName); + log.info("Created JCR workspace " + workspaceName); + } catch (RepositoryException e) { + throw new IllegalStateException("Cannot create workspace " + workspaceName, e); + } finally { + Jcr.logout(defaultAdminSession); + } + adminSession = NodeUtils.openDataAdminSession(repository, workspaceName); + } else + throw e1; + } + try { + if (prepareJcrTree(adminSession)) { + configurePrivileges(adminSession); + } + } catch (RepositoryException | IOException e) { + throw new IllegalStateException("Cannot initialise JCR data layer.", e); + } finally { + JcrUtils.logoutQuietly(adminSession); + } + } + + /** To be overridden. */ + protected Set getWorkspaceNames() { + return null; + } + + /** + * To be overridden in order to programmatically set relationships between + * roles. Does nothing by default. + */ + protected void configureStandardRoles() { + } + + /** + * Creates the base JCR tree structure expected for this app if necessary. + * + * Expects a clean session ({@link Session#hasPendingChanges()} should return + * false) and saves it once the changes have been done. Thus the session can be + * rolled back if an exception occurs. + * + * @return true if something as been updated + */ + public boolean prepareJcrTree(Session adminSession) throws RepositoryException, IOException { + return false; + } + + /** + * Adds app specific default privileges. + * + * Expects a clean session ({@link Session#hasPendingChanges()} should return + * false} and saves it once the changes have been done. Thus the session can be + * rolled back if an exception occurs. + * + * Warning: no check is done and corresponding privileges are always added, so + * only call this when necessary + */ + public void configurePrivileges(Session session) throws RepositoryException { + } + + /** The system roles that must be available in the system. */ + protected Set getRequiredRoles() { + return new HashSet<>(); + } + + public void destroy() { + + } + + /* + * UTILITIES + */ + + /** Create these roles as group if they don't exist. */ + protected void makeSureRolesExists(EnumSet enumSet) { + makeSureRolesExists(Distinguished.enumToDns(enumSet)); + } + + /** Create these roles as group if they don't exist. */ + protected void makeSureRolesExists(Set requiredRoles) { + if (requiredRoles == null) + return; + if (getUserAdmin() == null) { + log.warn("No user admin service available, cannot make sure that role exists"); + return; + } + for (String role : requiredRoles) { + Role systemRole = getUserAdmin().getRole(role); + if (systemRole == null) { + try { + getUserTransaction().begin(); + getUserAdmin().createRole(role, Role.GROUP); + getUserTransaction().commit(); + log.info("Created role " + role); + } catch (Exception e) { + try { + getUserTransaction().rollback(); + } catch (Exception e1) { + // silent + } + throw new IllegalStateException("Cannot create role " + role, e); + } + } + } + } + + /** Add a user or group to a group. */ + protected void addToGroup(String groupToAddDn, String groupDn) { + if (groupToAddDn.contentEquals(groupDn)) { + if (log.isTraceEnabled()) + log.trace("Ignore adding group " + groupDn + " to itself"); + return; + } + + if (getUserAdmin() == null) { + log.warn("No user admin service available, cannot add group " + groupToAddDn + " to " + groupDn); + return; + } + Group groupToAdd = (Group) getUserAdmin().getRole(groupToAddDn); + if (groupToAdd == null) + throw new IllegalArgumentException("Group " + groupToAddDn + " not found"); + Group group = (Group) getUserAdmin().getRole(groupDn); + if (group == null) + throw new IllegalArgumentException("Group " + groupDn + " not found"); + try { + getUserTransaction().begin(); + if (group.addMember(groupToAdd)) + log.info("Added " + groupToAddDn + " to " + group); + getUserTransaction().commit(); + } catch (Exception e) { + try { + getUserTransaction().rollback(); + } catch (Exception e1) { + // silent + } + throw new IllegalStateException("Cannot add " + groupToAddDn + " to " + groupDn); + } + } + + /* + * DEPENDENCY INJECTION + */ + public void setRepository(Repository repository) { + this.repository = repository; + } + +// public void setUserAdminService(UserAdminService userAdminService) { +// this.userAdminService = userAdminService; +// } + + protected UserTransaction getUserTransaction() { + return userTransaction; + } + + protected UserAdmin getUserAdmin() { + return userAdmin; + } + + public void setUserAdmin(UserAdmin userAdmin) { + this.userAdmin = userAdmin; + } + + public void setUserTransaction(UserTransaction userTransaction) { + this.userTransaction = userTransaction; + } + +} diff --git a/org.argeo.cms.jcr/src/org/argeo/maintenance/SimpleRoleRegistration.java b/org.argeo.cms.jcr/src/org/argeo/maintenance/SimpleRoleRegistration.java new file mode 100644 index 000000000..a30fe9796 --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/maintenance/SimpleRoleRegistration.java @@ -0,0 +1,87 @@ +package org.argeo.maintenance; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import javax.naming.InvalidNameException; +import javax.naming.ldap.LdapName; +import javax.transaction.UserTransaction; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.osgi.service.useradmin.Role; +import org.osgi.service.useradmin.UserAdmin; + +/** + * Register one or many roles via a user admin service. Does nothing if the role + * is already registered. + */ +public class SimpleRoleRegistration implements Runnable { + private final static Log log = LogFactory.getLog(SimpleRoleRegistration.class); + + private String role; + private List roles = new ArrayList(); + private UserAdmin userAdmin; + private UserTransaction userTransaction; + + @Override + public void run() { + try { + userTransaction.begin(); + if (role != null && !roleExists(role)) + newRole(toDn(role)); + + for (String r : roles) + if (!roleExists(r)) + newRole(toDn(r)); + userTransaction.commit(); + } catch (Exception e) { + try { + userTransaction.rollback(); + } catch (Exception e1) { + log.error("Cannot rollback", e1); + } + throw new IllegalArgumentException("Cannot add roles", e); + } + } + + private boolean roleExists(String role) { + return userAdmin.getRole(toDn(role).toString()) != null; + } + + protected void newRole(LdapName r) { + userAdmin.createRole(r.toString(), Role.GROUP); + log.info("Added role " + r + " required by application."); + } + + public void register(UserAdmin userAdminService, Map properties) { + this.userAdmin = userAdminService; + run(); + } + + protected LdapName toDn(String name) { + try { + return new LdapName("cn=" + name + ",ou=roles,ou=node"); + } catch (InvalidNameException e) { + throw new IllegalArgumentException("Badly formatted role name " + name, e); + } + } + + public void setRole(String role) { + this.role = role; + } + + public void setRoles(List roles) { + this.roles = roles; + } + + public void setUserAdmin(UserAdmin userAdminService) { + this.userAdmin = userAdminService; + } + + public void setUserTransaction(UserTransaction userTransaction) { + this.userTransaction = userTransaction; + } + +} diff --git a/org.argeo.cms.jcr/src/org/argeo/maintenance/backup/BackupContentHandler.java b/org.argeo.cms.jcr/src/org/argeo/maintenance/backup/BackupContentHandler.java new file mode 100644 index 000000000..ef83c1ff9 --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/maintenance/backup/BackupContentHandler.java @@ -0,0 +1,257 @@ +package org.argeo.maintenance.backup; + +import java.io.IOException; +import java.io.InputStream; +import java.io.Writer; +import java.util.Arrays; +import java.util.Base64; +import java.util.Set; +import java.util.TreeSet; + +import javax.jcr.Binary; +import javax.jcr.Node; +import javax.jcr.RepositoryException; +import javax.jcr.Session; + +import org.apache.commons.io.IOUtils; +import org.argeo.jcr.Jcr; +import org.argeo.jcr.JcrException; +import org.xml.sax.Attributes; +import org.xml.sax.SAXException; +import org.xml.sax.helpers.DefaultHandler; + +/** XML handler serialising a JCR system view. */ +public class BackupContentHandler extends DefaultHandler { + final static int MAX_DEPTH = 1024; + final static String SV_NAMESPACE_URI = "http://www.jcp.org/jcr/sv/1.0"; + final static String SV_PREFIX = "sv"; + // elements + final static String NODE = "node"; + final static String PROPERTY = "property"; + final static String VALUE = "value"; + // attributes + final static String NAME = "name"; + final static String MULTIPLE = "multiple"; + final static String TYPE = "type"; + + // values + final static String BINARY = "Binary"; + final static String JCR_CONTENT = "jcr:content"; + + private Writer out; + private Session session; + private Set contentPaths = new TreeSet<>(); + + boolean prettyPrint = true; + + private final String parentPath; + +// private boolean inSystem = false; + + public BackupContentHandler(Writer out, Node node) { + super(); + this.out = out; + this.session = Jcr.getSession(node); + parentPath = Jcr.getParentPath(node); + } + + private int currentDepth = -1; + private String[] currentPath = new String[MAX_DEPTH]; + + private boolean currentPropertyIsMultiple = false; + private String currentEncoded = null; + private Base64.Encoder base64encore = Base64.getEncoder(); + + @Override + public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException { + boolean isNode; + boolean isProperty; + switch (localName) { + case NODE: + isNode = true; + isProperty = false; + break; + case PROPERTY: + isNode = false; + isProperty = true; + break; + default: + isNode = false; + isProperty = false; + } + + if (isNode) { + String nodeName = attributes.getValue(SV_NAMESPACE_URI, NAME); + currentDepth = currentDepth + 1; +// if (currentDepth >= 0) + currentPath[currentDepth] = nodeName; +// System.out.println(getCurrentPath() + " , depth=" + currentDepth); +// if ("jcr:system".equals(nodeName)) { +// inSystem = true; +// } + } +// if (inSystem) +// return; + + if (SV_NAMESPACE_URI.equals(uri)) + try { + if (prettyPrint) { + if (isNode) { + out.write(spaces()); + out.write("\n"); + out.write(spaces()); + } else if (isProperty) + out.write(spaces()); + else if (currentPropertyIsMultiple) + out.write(spaces()); + } + + out.write("<"); + out.write(SV_PREFIX + ":" + localName); + if (isProperty) + currentPropertyIsMultiple = false; // always reset + for (int i = 0; i < attributes.getLength(); i++) { + String ns = attributes.getURI(i); + if (SV_NAMESPACE_URI.equals(ns)) { + String attrName = attributes.getLocalName(i); + String attrValue = attributes.getValue(i); + out.write(" "); + out.write(SV_PREFIX + ":" + attrName); + out.write("="); + out.write("\""); + out.write(attrValue); + out.write("\""); + if (isProperty) { + if (MULTIPLE.equals(attrName)) + currentPropertyIsMultiple = Boolean.parseBoolean(attrValue); + else if (TYPE.equals(attrName)) { + if (BINARY.equals(attrValue)) { + if (JCR_CONTENT.equals(getCurrentName())) { + contentPaths.add(getCurrentJcrPath()); + } else { + Binary binary = session.getNode(getCurrentJcrPath()).getProperty(attrName) + .getBinary(); + try (InputStream in = binary.getStream()) { + currentEncoded = base64encore.encodeToString(IOUtils.toByteArray(in)); + } finally { + + } + } + } + } + } + } + } + if (isNode && currentDepth == 0) { + // out.write(" xmlns=\"" + SV_NAMESPACE_URI + "\""); + out.write(" xmlns:" + SV_PREFIX + "=\"" + SV_NAMESPACE_URI + "\""); + } + out.write(">"); + + if (prettyPrint) + if (isNode) + out.write("\n"); + else if (isProperty && currentPropertyIsMultiple) + out.write("\n"); + } catch (IOException e) { + throw new RuntimeException(e); + } catch (RepositoryException e) { + throw new JcrException(e); + } + } + + @Override + public void endElement(String uri, String localName, String qName) throws SAXException { + boolean isNode = localName.equals(NODE); + boolean isValue = localName.equals(VALUE); + if (prettyPrint) + if (!isValue) + try { + if (isNode || currentPropertyIsMultiple) + out.write(spaces()); + } catch (IOException e1) { + throw new RuntimeException(e1); + } + if (isNode) { +// System.out.println("endElement " + getCurrentPath() + " , depth=" + currentDepth); +// if (currentDepth > 0) + currentPath[currentDepth] = null; + currentDepth = currentDepth - 1; +// if (inSystem) { +// // System.out.println("Skip " + getCurrentPath()+" , +// // currentDepth="+currentDepth); +// if (currentDepth == 0) { +// inSystem = false; +// return; +// } +// } + } +// if (inSystem) +// return; + if (SV_NAMESPACE_URI.equals(uri)) + try { + if (isValue && currentEncoded != null) { + out.write(currentEncoded); + } + currentEncoded = null; + out.write(""); + if (prettyPrint) + if (!isValue) + out.write("\n"); + else { + if (currentPropertyIsMultiple) + out.write("\n"); + } + if (currentDepth == 0) + out.flush(); + } catch (IOException e) { + throw new RuntimeException(e); + } + + } + + private char[] spaces() { + char[] arr = new char[currentDepth]; + Arrays.fill(arr, ' '); + return arr; + } + + @Override + public void characters(char[] ch, int start, int length) throws SAXException { +// if (inSystem) +// return; + try { + out.write(ch, start, length); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + protected String getCurrentName() { + assert currentDepth >= 0; +// if (currentDepth == 0) +// return "jcr:root"; + return currentPath[currentDepth]; + } + + protected String getCurrentJcrPath() { +// if (currentDepth == 0) +// return "/"; + StringBuilder sb = new StringBuilder(parentPath.equals("/") ? "" : parentPath); + for (int i = 0; i <= currentDepth; i++) { +// if (i != 0) + sb.append('/'); + sb.append(currentPath[i]); + } + return sb.toString(); + } + + public Set getContentPaths() { + return contentPaths; + } + +} diff --git a/org.argeo.cms.jcr/src/org/argeo/maintenance/backup/LogicalBackup.java b/org.argeo.cms.jcr/src/org/argeo/maintenance/backup/LogicalBackup.java new file mode 100644 index 000000000..60e8f8e5d --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/maintenance/backup/LogicalBackup.java @@ -0,0 +1,449 @@ +package org.argeo.maintenance.backup; + +import java.io.BufferedWriter; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.Writer; +import java.net.URI; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Dictionary; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.jar.JarOutputStream; +import java.util.jar.Manifest; +import java.util.zip.ZipEntry; +import java.util.zip.ZipException; +import java.util.zip.ZipOutputStream; + +import javax.jcr.Binary; +import javax.jcr.Node; +import javax.jcr.NodeIterator; +import javax.jcr.Property; +import javax.jcr.Repository; +import javax.jcr.RepositoryException; +import javax.jcr.RepositoryFactory; +import javax.jcr.Session; + +import org.apache.commons.io.IOUtils; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.jackrabbit.api.JackrabbitSession; +import org.apache.jackrabbit.api.JackrabbitValue; +import org.argeo.api.NodeConstants; +import org.argeo.api.NodeUtils; +import org.argeo.jackrabbit.client.ClientDavexRepositoryFactory; +import org.argeo.jcr.Jcr; +import org.argeo.jcr.JcrException; +import org.argeo.jcr.JcrUtils; +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleContext; + +/** + * Performs a backup of the data based only on programmatic interfaces. Useful + * for migration or live backup. Physical backups of the underlying file + * systems, databases, LDAP servers, etc. should be performed for disaster + * recovery. + */ +public class LogicalBackup implements Runnable { + private final static Log log = LogFactory.getLog(LogicalBackup.class); + + public final static String WORKSPACES_BASE = "workspaces/"; + public final static String FILES_BASE = "files/"; + public final static String OSGI_BASE = "share/osgi/"; + + public final static String JCR_SYSTEM = "jcr:system"; + public final static String JCR_VERSION_STORAGE_PATH = "/jcr:system/jcr:versionStorage"; + + private final Repository repository; + private String defaultWorkspace; + private final BundleContext bundleContext; + + private final ZipOutputStream zout; + private final Path basePath; + + private ExecutorService executorService; + + private boolean performSoftwareBackup = false; + + private Map checksums = new TreeMap<>(); + + private int threadCount = 5; + + private boolean backupFailed = false; + + public LogicalBackup(BundleContext bundleContext, Repository repository, Path basePath) { + this.repository = repository; + this.zout = null; + this.basePath = basePath; + this.bundleContext = bundleContext; + } + + @Override + public void run() { + try { + log.info("Start logical backup to " + basePath); + perform(); + } catch (Exception e) { + log.error("Unexpected exception when performing logical backup", e); + throw new IllegalStateException("Logical backup failed", e); + } + + } + + public void perform() throws RepositoryException, IOException { + if (executorService != null && !executorService.isTerminated()) + throw new IllegalStateException("Another backup is running"); + executorService = Executors.newFixedThreadPool(threadCount); + long begin = System.currentTimeMillis(); + // software backup + if (bundleContext != null && performSoftwareBackup) + executorService.submit(() -> performSoftwareBackup(bundleContext)); + + // data backup + Session defaultSession = login(null); + defaultWorkspace = defaultSession.getWorkspace().getName(); + try { + String[] workspaceNames = defaultSession.getWorkspace().getAccessibleWorkspaceNames(); + workspaces: for (String workspaceName : workspaceNames) { + if ("security".equals(workspaceName)) + continue workspaces; + performDataBackup(workspaceName); + } + } finally { + JcrUtils.logoutQuietly(defaultSession); + executorService.shutdown(); + try { + executorService.awaitTermination(24, TimeUnit.HOURS); + } catch (InterruptedException e) { + // silent + throw new IllegalStateException("Backup was interrupted before completion", e); + } + } + // versions + executorService = Executors.newFixedThreadPool(threadCount); + try { + performVersionsBackup(); + } finally { + executorService.shutdown(); + try { + executorService.awaitTermination(24, TimeUnit.HOURS); + } catch (InterruptedException e) { + // silent + throw new IllegalStateException("Backup was interrupted before completion", e); + } + } + long duration = System.currentTimeMillis() - begin; + if (isBackupFailed()) + log.info("System logical backup failed after " + (duration / 60000) + "min " + (duration / 1000) + "s"); + else + log.info("System logical backup completed in " + (duration / 60000) + "min " + (duration / 1000) + "s"); + } + + protected void performDataBackup(String workspaceName) throws RepositoryException, IOException { + Session session = login(workspaceName); + try { + nodes: for (NodeIterator nit = session.getRootNode().getNodes(); nit.hasNext();) { + if (isBackupFailed()) + return; + Node nodeToExport = nit.nextNode(); + if (JCR_SYSTEM.equals(nodeToExport.getName())) + continue nodes; + String nodePath = nodeToExport.getPath(); + Future> contentPathsFuture = executorService + .submit(() -> performNodeBackup(workspaceName, nodePath)); + executorService.submit(() -> performFilesBackup(workspaceName, contentPathsFuture)); + } + } finally { + Jcr.logout(session); + } + } + + protected void performVersionsBackup() throws RepositoryException, IOException { + Session session = login(defaultWorkspace); + Node versionStorageNode = session.getNode(JCR_VERSION_STORAGE_PATH); + try { + for (NodeIterator nit = versionStorageNode.getNodes(); nit.hasNext();) { + Node nodeToExport = nit.nextNode(); + String nodePath = nodeToExport.getPath(); + if (isBackupFailed()) + return; + Future> contentPathsFuture = executorService + .submit(() -> performNodeBackup(defaultWorkspace, nodePath)); + executorService.submit(() -> performFilesBackup(defaultWorkspace, contentPathsFuture)); + } + } finally { + Jcr.logout(session); + } + + } + + protected Set performNodeBackup(String workspaceName, String nodePath) { + Session session = login(workspaceName); + try { + Node nodeToExport = session.getNode(nodePath); +// String nodeName = nodeToExport.getName(); +// if (nodeName.startsWith("jcr:") || nodeName.startsWith("rep:")) +// continue nodes; +// // TODO make it more robust / configurable +// if (nodeName.equals("user")) +// continue nodes; + String relativePath = WORKSPACES_BASE + workspaceName + nodePath + ".xml"; + OutputStream xmlOut = openOutputStream(relativePath); + BackupContentHandler contentHandler; + try (Writer writer = new BufferedWriter(new OutputStreamWriter(xmlOut, StandardCharsets.UTF_8))) { + contentHandler = new BackupContentHandler(writer, nodeToExport); + session.exportSystemView(nodeToExport.getPath(), contentHandler, true, false); + if (log.isDebugEnabled()) + log.debug(workspaceName + ":" + nodePath + " metadata exported to " + relativePath); + } + + // Files + Set contentPaths = contentHandler.getContentPaths(); + return contentPaths; + } catch (Exception e) { + markBackupFailed("Cannot backup node " + workspaceName + ":" + nodePath, e); + throw new ThreadDeath(); + } finally { + Jcr.logout(session); + } + } + + protected void performFilesBackup(String workspaceName, Future> contentPathsFuture) { + Set contentPaths; + try { + contentPaths = contentPathsFuture.get(24, TimeUnit.HOURS); + } catch (InterruptedException | ExecutionException | TimeoutException e1) { + markBackupFailed("Cannot retrieve content paths for workspace " + workspaceName, e1); + return; + } + if (contentPaths == null || contentPaths.size() == 0) + return; + Session session = login(workspaceName); + try { + String workspacesFilesBasePath = FILES_BASE + workspaceName; + for (String path : contentPaths) { + if (isBackupFailed()) + return; + Node contentNode = session.getNode(path); + Binary binary = null; + try { + binary = contentNode.getProperty(Property.JCR_DATA).getBinary(); + String fileRelativePath = workspacesFilesBasePath + contentNode.getParent().getPath(); + + // checksum + boolean skip = false; + String checksum = null; + if (session instanceof JackrabbitSession) { + JackrabbitValue value = (JackrabbitValue) contentNode.getProperty(Property.JCR_DATA).getValue(); +// ReferenceBinary referenceBinary = (ReferenceBinary) binary; + checksum = value.getContentIdentity(); + } + if (checksum != null) { + if (!checksums.containsKey(checksum)) { + checksums.put(checksum, fileRelativePath); + } else { + skip = true; + String sourcePath = checksums.get(checksum); + if (log.isTraceEnabled()) + log.trace(fileRelativePath + " : already " + sourcePath + " with checksum " + checksum); + createLink(sourcePath, fileRelativePath); + try (Writer writerSum = new OutputStreamWriter( + openOutputStream(fileRelativePath + ".sha256"), StandardCharsets.UTF_8)) { + writerSum.write(checksum); + } + } + } + + // copy file + if (!skip) + try (InputStream in = binary.getStream(); + OutputStream out = openOutputStream(fileRelativePath)) { + IOUtils.copy(in, out); + if (log.isTraceEnabled()) + log.trace("Workspace " + workspaceName + ": file content exported to " + + fileRelativePath); + } + } finally { + JcrUtils.closeQuietly(binary); + } + } + if (log.isDebugEnabled()) + log.debug(workspaceName + ":" + contentPaths.size() + " files exported to " + workspacesFilesBasePath); + } catch (Exception e) { + markBackupFailed("Cannot backup files from " + workspaceName + ":", e); + } finally { + Jcr.logout(session); + } + } + + protected OutputStream openOutputStream(String relativePath) throws IOException { + if (zout != null) { + ZipEntry entry = new ZipEntry(relativePath); + zout.putNextEntry(entry); + return zout; + } else if (basePath != null) { + Path targetPath = basePath.resolve(Paths.get(relativePath)); + Files.createDirectories(targetPath.getParent()); + return Files.newOutputStream(targetPath); + } else { + throw new UnsupportedOperationException(); + } + } + + protected void createLink(String source, String target) throws IOException { + if (zout != null) { + // TODO implement for zip + throw new UnsupportedOperationException(); + } else if (basePath != null) { + Path sourcePath = basePath.resolve(Paths.get(source)); + Path targetPath = basePath.resolve(Paths.get(target)); + Path relativeSource = targetPath.getParent().relativize(sourcePath); + Files.createDirectories(targetPath.getParent()); + Files.createSymbolicLink(targetPath, relativeSource); + } else { + throw new UnsupportedOperationException(); + } + } + + protected void closeOutputStream(String relativePath, OutputStream out) throws IOException { + if (zout != null) { + zout.closeEntry(); + } else if (basePath != null) { + out.close(); + } else { + throw new UnsupportedOperationException(); + } + } + + protected Session login(String workspaceName) { + if (bundleContext != null) {// local + return NodeUtils.openDataAdminSession(repository, workspaceName); + } else {// remote + try { + return repository.login(workspaceName); + } catch (RepositoryException e) { + throw new JcrException(e); + } + } + } + + public final static void main(String[] args) throws Exception { + if (args.length == 0) { + printUsage("No argument"); + System.exit(1); + } + URI uri = new URI(args[0]); + Repository repository = createRemoteRepository(uri); + Path basePath = args.length > 1 ? Paths.get(args[1]) : Paths.get(System.getProperty("user.dir")); + if (!Files.exists(basePath)) + Files.createDirectories(basePath); + LogicalBackup backup = new LogicalBackup(null, repository, basePath); + backup.run(); + } + + private static void printUsage(String errorMessage) { + if (errorMessage != null) + System.err.println(errorMessage); + System.out.println("Usage: LogicalBackup []"); + + } + + protected static Repository createRemoteRepository(URI uri) throws RepositoryException { + RepositoryFactory repositoryFactory = new ClientDavexRepositoryFactory(); + Map params = new HashMap(); + params.put(ClientDavexRepositoryFactory.JACKRABBIT_DAVEX_URI, uri.toString()); + // TODO make it configurable + params.put(ClientDavexRepositoryFactory.JACKRABBIT_REMOTE_DEFAULT_WORKSPACE, NodeConstants.SYS_WORKSPACE); + return repositoryFactory.getRepository(params); + } + + public void performSoftwareBackup(BundleContext bundleContext) { + String bootBasePath = OSGI_BASE + "boot"; + Bundle[] bundles = bundleContext.getBundles(); + for (Bundle bundle : bundles) { + String relativePath = bootBasePath + "/" + bundle.getSymbolicName() + ".jar"; + Dictionary headers = bundle.getHeaders(); + Manifest manifest = new Manifest(); + Enumeration headerKeys = headers.keys(); + while (headerKeys.hasMoreElements()) { + String headerKey = headerKeys.nextElement(); + String headerValue = headers.get(headerKey); + manifest.getMainAttributes().putValue(headerKey, headerValue); + } + try (JarOutputStream jarOut = new JarOutputStream(openOutputStream(relativePath), manifest)) { + Enumeration resourcePaths = bundle.findEntries("/", "*", true); + resources: while (resourcePaths.hasMoreElements()) { + URL entryUrl = resourcePaths.nextElement(); + String entryPath = entryUrl.getPath(); + if (entryPath.equals("")) + continue resources; + if (entryPath.endsWith("/")) + continue resources; + String entryName = entryPath.substring(1);// remove first '/' + if (entryUrl.getPath().equals("/META-INF/")) + continue resources; + if (entryUrl.getPath().equals("/META-INF/MANIFEST.MF")) + continue resources; + // dev + if (entryUrl.getPath().startsWith("/target")) + continue resources; + if (entryUrl.getPath().startsWith("/src")) + continue resources; + if (entryUrl.getPath().startsWith("/ext")) + continue resources; + + if (entryName.startsWith("bin/")) {// dev + entryName = entryName.substring("bin/".length()); + } + + ZipEntry entry = new ZipEntry(entryName); + try (InputStream in = entryUrl.openStream()) { + try { + jarOut.putNextEntry(entry); + } catch (ZipException e) {// duplicate + continue resources; + } + IOUtils.copy(in, jarOut); + jarOut.closeEntry(); +// log.info(entryUrl); + } catch (FileNotFoundException e) { + log.warn(entryUrl + ": " + e.getMessage()); + } + } + } catch (IOException e1) { + throw new RuntimeException("Cannot export bundle " + bundle, e1); + } + } + if (log.isDebugEnabled()) + log.debug(bundles.length + " OSGi bundles exported to " + bootBasePath); + + } + + protected synchronized void markBackupFailed(Object message, Exception e) { + log.error(message, e); + backupFailed = true; + notifyAll(); + if (executorService != null) + executorService.shutdownNow(); + } + + protected boolean isBackupFailed() { + return backupFailed; + } +} diff --git a/org.argeo.cms.jcr/src/org/argeo/maintenance/backup/LogicalRestore.java b/org.argeo.cms.jcr/src/org/argeo/maintenance/backup/LogicalRestore.java new file mode 100644 index 000000000..a12bb41c9 --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/maintenance/backup/LogicalRestore.java @@ -0,0 +1,86 @@ +package org.argeo.maintenance.backup; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.DirectoryStream; +import java.nio.file.Files; +import java.nio.file.Path; + +import javax.jcr.ImportUUIDBehavior; +import javax.jcr.Repository; +import javax.jcr.RepositoryException; +import javax.jcr.Session; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.argeo.api.NodeConstants; +import org.argeo.api.NodeUtils; +import org.argeo.jcr.Jcr; +import org.argeo.jcr.JcrException; +import org.argeo.jcr.JcrUtils; +import org.osgi.framework.BundleContext; + +/** Restores a backup in the format defined by {@link LogicalBackup}. */ +public class LogicalRestore implements Runnable { + private final static Log log = LogFactory.getLog(LogicalRestore.class); + + private final Repository repository; + private final BundleContext bundleContext; + private final Path basePath; + + public LogicalRestore(BundleContext bundleContext, Repository repository, Path basePath) { + this.repository = repository; + this.basePath = basePath; + this.bundleContext = bundleContext; + } + + @Override + public void run() { + Path workspaces = basePath.resolve(LogicalBackup.WORKSPACES_BASE); + try { + // import jcr:system first +// Session defaultSession = NodeUtils.openDataAdminSession(repository, null); +// try (DirectoryStream xmls = Files.newDirectoryStream( +// workspaces.resolve(NodeConstants.SYS_WORKSPACE + LogicalBackup.JCR_VERSION_STORAGE_PATH), +// "*.xml")) { +// for (Path xml : xmls) { +// try (InputStream in = Files.newInputStream(xml)) { +// defaultSession.getWorkspace().importXML(LogicalBackup.JCR_VERSION_STORAGE_PATH, in, +// ImportUUIDBehavior.IMPORT_UUID_COLLISION_REPLACE_EXISTING); +// if (log.isDebugEnabled()) +// log.debug("Restored " + xml + " to " + defaultSession.getWorkspace().getName() + ":"); +// } +// } +// } finally { +// Jcr.logout(defaultSession); +// } + + // non-system content + try (DirectoryStream workspaceDirs = Files.newDirectoryStream(workspaces)) { + for (Path workspacePath : workspaceDirs) { + String workspaceName = workspacePath.getFileName().toString(); + Session session = JcrUtils.loginOrCreateWorkspace(repository, workspaceName); + try (DirectoryStream xmls = Files.newDirectoryStream(workspacePath, "*.xml")) { + xmls: for (Path xml : xmls) { + if (xml.getFileName().toString().startsWith("rep:")) + continue xmls; + try (InputStream in = Files.newInputStream(xml)) { + session.getWorkspace().importXML("/", in, + ImportUUIDBehavior.IMPORT_UUID_COLLISION_REPLACE_EXISTING); + if (log.isDebugEnabled()) + log.debug("Restored " + xml + " to workspace " + workspaceName); + } + } + } finally { + Jcr.logout(session); + } + } + } + } catch (IOException e) { + throw new RuntimeException("Cannot restore backup from " + basePath, e); + } catch (RepositoryException e) { + throw new JcrException("Cannot restore backup from " + basePath, e); + } + } + +} diff --git a/org.argeo.cms.jcr/src/org/argeo/maintenance/backup/package-info.java b/org.argeo.cms.jcr/src/org/argeo/maintenance/backup/package-info.java new file mode 100644 index 000000000..a61e19bd4 --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/maintenance/backup/package-info.java @@ -0,0 +1,2 @@ +/** Argeo Node backup utilities. */ +package org.argeo.maintenance.backup; \ No newline at end of file diff --git a/org.argeo.cms.jcr/src/org/argeo/maintenance/internal/Activator.java b/org.argeo.cms.jcr/src/org/argeo/maintenance/internal/Activator.java new file mode 100644 index 000000000..ef40ab3a3 --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/maintenance/internal/Activator.java @@ -0,0 +1,27 @@ +package org.argeo.maintenance.internal; + +import java.nio.file.Path; +import java.nio.file.Paths; + +import javax.jcr.Repository; + +import org.argeo.maintenance.backup.LogicalBackup; +import org.osgi.framework.BundleActivator; +import org.osgi.framework.BundleContext; + +public class Activator implements BundleActivator { + + @Override + public void start(BundleContext context) throws Exception { + // Start backup + Repository repository = context.getService(context.getServiceReference(Repository.class)); + Path basePath = Paths.get(System.getProperty("user.dir"), "backup"); + LogicalBackup backup = new LogicalBackup(context, repository, basePath); + backup.run(); + } + + @Override + public void stop(BundleContext context) throws Exception { + } + +} diff --git a/org.argeo.cms.jcr/src/org/argeo/maintenance/package-info.java b/org.argeo.cms.jcr/src/org/argeo/maintenance/package-info.java new file mode 100644 index 000000000..1ce974c6f --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/maintenance/package-info.java @@ -0,0 +1,2 @@ +/** Utilities for the maintenance of an Argeo Node. */ +package org.argeo.maintenance; \ No newline at end of file diff --git a/org.argeo.cms/pom.xml b/org.argeo.cms/pom.xml index e19f92e01..0a313033b 100644 --- a/org.argeo.cms/pom.xml +++ b/org.argeo.cms/pom.xml @@ -18,7 +18,7 @@ org.argeo.commons - org.argeo.jcr + org.argeo.cms.jcr 2.3-SNAPSHOT @@ -31,10 +31,10 @@ org.argeo.core 2.3-SNAPSHOT - - org.argeo.commons - org.argeo.maintenance - 2.3-SNAPSHOT - + + + + + \ No newline at end of file diff --git a/org.argeo.core/.settings/org.eclipse.jdt.core.prefs b/org.argeo.core/.settings/org.eclipse.jdt.core.prefs deleted file mode 100644 index 7e2e11935..000000000 --- a/org.argeo.core/.settings/org.eclipse.jdt.core.prefs +++ /dev/null @@ -1,101 +0,0 @@ -eclipse.preferences.version=1 -org.eclipse.jdt.core.compiler.annotation.inheritNullAnnotations=disabled -org.eclipse.jdt.core.compiler.annotation.missingNonNullByDefaultAnnotation=ignore -org.eclipse.jdt.core.compiler.annotation.nonnull=org.eclipse.jdt.annotation.NonNull -org.eclipse.jdt.core.compiler.annotation.nonnull.secondary= -org.eclipse.jdt.core.compiler.annotation.nonnullbydefault=org.eclipse.jdt.annotation.NonNullByDefault -org.eclipse.jdt.core.compiler.annotation.nonnullbydefault.secondary= -org.eclipse.jdt.core.compiler.annotation.nullable=org.eclipse.jdt.annotation.Nullable -org.eclipse.jdt.core.compiler.annotation.nullable.secondary= -org.eclipse.jdt.core.compiler.annotation.nullanalysis=disabled -org.eclipse.jdt.core.compiler.problem.APILeak=warning -org.eclipse.jdt.core.compiler.problem.annotationSuperInterface=warning -org.eclipse.jdt.core.compiler.problem.autoboxing=ignore -org.eclipse.jdt.core.compiler.problem.comparingIdentical=warning -org.eclipse.jdt.core.compiler.problem.deadCode=warning -org.eclipse.jdt.core.compiler.problem.deprecation=warning -org.eclipse.jdt.core.compiler.problem.deprecationInDeprecatedCode=disabled -org.eclipse.jdt.core.compiler.problem.deprecationWhenOverridingDeprecatedMethod=disabled -org.eclipse.jdt.core.compiler.problem.discouragedReference=warning -org.eclipse.jdt.core.compiler.problem.emptyStatement=ignore -org.eclipse.jdt.core.compiler.problem.explicitlyClosedAutoCloseable=ignore -org.eclipse.jdt.core.compiler.problem.fallthroughCase=ignore -org.eclipse.jdt.core.compiler.problem.fatalOptionalError=disabled -org.eclipse.jdt.core.compiler.problem.fieldHiding=ignore -org.eclipse.jdt.core.compiler.problem.finalParameterBound=warning -org.eclipse.jdt.core.compiler.problem.finallyBlockNotCompletingNormally=warning -org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning -org.eclipse.jdt.core.compiler.problem.hiddenCatchBlock=warning -org.eclipse.jdt.core.compiler.problem.includeNullInfoFromAsserts=disabled -org.eclipse.jdt.core.compiler.problem.incompatibleNonInheritedInterfaceMethod=warning -org.eclipse.jdt.core.compiler.problem.incompleteEnumSwitch=warning -org.eclipse.jdt.core.compiler.problem.indirectStaticAccess=ignore -org.eclipse.jdt.core.compiler.problem.localVariableHiding=ignore -org.eclipse.jdt.core.compiler.problem.methodWithConstructorName=warning -org.eclipse.jdt.core.compiler.problem.missingDefaultCase=ignore -org.eclipse.jdt.core.compiler.problem.missingDeprecatedAnnotation=ignore -org.eclipse.jdt.core.compiler.problem.missingEnumCaseDespiteDefault=disabled -org.eclipse.jdt.core.compiler.problem.missingHashCodeMethod=ignore -org.eclipse.jdt.core.compiler.problem.missingOverrideAnnotation=ignore -org.eclipse.jdt.core.compiler.problem.missingOverrideAnnotationForInterfaceMethodImplementation=enabled -org.eclipse.jdt.core.compiler.problem.missingSerialVersion=warning -org.eclipse.jdt.core.compiler.problem.missingSynchronizedOnInheritedMethod=ignore -org.eclipse.jdt.core.compiler.problem.noEffectAssignment=warning -org.eclipse.jdt.core.compiler.problem.noImplicitStringConversion=warning -org.eclipse.jdt.core.compiler.problem.nonExternalizedStringLiteral=ignore -org.eclipse.jdt.core.compiler.problem.nonnullParameterAnnotationDropped=warning -org.eclipse.jdt.core.compiler.problem.nonnullTypeVariableFromLegacyInvocation=warning -org.eclipse.jdt.core.compiler.problem.nullAnnotationInferenceConflict=error -org.eclipse.jdt.core.compiler.problem.nullReference=warning -org.eclipse.jdt.core.compiler.problem.nullSpecViolation=error -org.eclipse.jdt.core.compiler.problem.nullUncheckedConversion=warning -org.eclipse.jdt.core.compiler.problem.overridingPackageDefaultMethod=warning -org.eclipse.jdt.core.compiler.problem.parameterAssignment=ignore -org.eclipse.jdt.core.compiler.problem.pessimisticNullAnalysisForFreeTypeVariables=warning -org.eclipse.jdt.core.compiler.problem.possibleAccidentalBooleanAssignment=ignore -org.eclipse.jdt.core.compiler.problem.potentialNullReference=ignore -org.eclipse.jdt.core.compiler.problem.potentiallyUnclosedCloseable=ignore -org.eclipse.jdt.core.compiler.problem.rawTypeReference=warning -org.eclipse.jdt.core.compiler.problem.redundantNullAnnotation=warning -org.eclipse.jdt.core.compiler.problem.redundantNullCheck=ignore -org.eclipse.jdt.core.compiler.problem.redundantSpecificationOfTypeArguments=ignore -org.eclipse.jdt.core.compiler.problem.redundantSuperinterface=ignore -org.eclipse.jdt.core.compiler.problem.reportMethodCanBePotentiallyStatic=ignore -org.eclipse.jdt.core.compiler.problem.reportMethodCanBeStatic=ignore -org.eclipse.jdt.core.compiler.problem.specialParameterHidingField=disabled -org.eclipse.jdt.core.compiler.problem.staticAccessReceiver=warning -org.eclipse.jdt.core.compiler.problem.suppressOptionalErrors=disabled -org.eclipse.jdt.core.compiler.problem.suppressWarnings=enabled -org.eclipse.jdt.core.compiler.problem.syntacticNullAnalysisForFields=disabled -org.eclipse.jdt.core.compiler.problem.syntheticAccessEmulation=ignore -org.eclipse.jdt.core.compiler.problem.terminalDeprecation=warning -org.eclipse.jdt.core.compiler.problem.typeParameterHiding=warning -org.eclipse.jdt.core.compiler.problem.unavoidableGenericTypeProblems=enabled -org.eclipse.jdt.core.compiler.problem.uncheckedTypeOperation=warning -org.eclipse.jdt.core.compiler.problem.unclosedCloseable=warning -org.eclipse.jdt.core.compiler.problem.undocumentedEmptyBlock=ignore -org.eclipse.jdt.core.compiler.problem.unhandledWarningToken=warning -org.eclipse.jdt.core.compiler.problem.unlikelyCollectionMethodArgumentType=warning -org.eclipse.jdt.core.compiler.problem.unlikelyCollectionMethodArgumentTypeStrict=disabled -org.eclipse.jdt.core.compiler.problem.unlikelyEqualsArgumentType=info -org.eclipse.jdt.core.compiler.problem.unnecessaryElse=ignore -org.eclipse.jdt.core.compiler.problem.unnecessaryTypeCheck=ignore -org.eclipse.jdt.core.compiler.problem.unqualifiedFieldAccess=ignore -org.eclipse.jdt.core.compiler.problem.unstableAutoModuleName=warning -org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownException=ignore -org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionExemptExceptionAndThrowable=enabled -org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionIncludeDocCommentReference=enabled -org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionWhenOverriding=disabled -org.eclipse.jdt.core.compiler.problem.unusedExceptionParameter=ignore -org.eclipse.jdt.core.compiler.problem.unusedImport=warning -org.eclipse.jdt.core.compiler.problem.unusedLabel=warning -org.eclipse.jdt.core.compiler.problem.unusedLocal=warning -org.eclipse.jdt.core.compiler.problem.unusedObjectAllocation=ignore -org.eclipse.jdt.core.compiler.problem.unusedParameter=ignore -org.eclipse.jdt.core.compiler.problem.unusedParameterIncludeDocCommentReference=enabled -org.eclipse.jdt.core.compiler.problem.unusedParameterWhenImplementingAbstract=disabled -org.eclipse.jdt.core.compiler.problem.unusedParameterWhenOverridingConcrete=disabled -org.eclipse.jdt.core.compiler.problem.unusedPrivateMember=warning -org.eclipse.jdt.core.compiler.problem.unusedTypeParameter=ignore -org.eclipse.jdt.core.compiler.problem.unusedWarningToken=warning -org.eclipse.jdt.core.compiler.problem.varargsArgumentNeedCast=warning diff --git a/org.argeo.core/bnd.bnd b/org.argeo.core/bnd.bnd index f096e4f29..e69de29bb 100644 --- a/org.argeo.core/bnd.bnd +++ b/org.argeo.core/bnd.bnd @@ -1,9 +0,0 @@ -Import-Package:\ -org.apache.jackrabbit.api,\ -org.apache.jackrabbit.commons,\ -org.apache.jackrabbit.spi,\ -org.apache.jackrabbit.spi2dav,\ -org.apache.jackrabbit.spi2davex,\ -org.apache.jackrabbit.webdav,\ -junit.*;resolution:=optional,\ -* \ No newline at end of file diff --git a/org.argeo.core/build.properties b/org.argeo.core/build.properties index 49a93ba4a..353d0422a 100644 --- a/org.argeo.core/build.properties +++ b/org.argeo.core/build.properties @@ -1,29 +1,6 @@ -source.. = src/ +source.. = src/,\ + ext/test/ 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 \ No newline at end of file diff --git a/org.argeo.core/ext/test/org/argeo/jcr/fs/JcrFileSystemTest.java b/org.argeo.core/ext/test/org/argeo/jcr/fs/JcrFileSystemTest.java deleted file mode 100644 index 2d03b8f2c..000000000 --- a/org.argeo.core/ext/test/org/argeo/jcr/fs/JcrFileSystemTest.java +++ /dev/null @@ -1,191 +0,0 @@ -package org.argeo.jcr.fs; - -import java.io.InputStream; -import java.io.OutputStream; -import java.net.URI; -import java.nio.file.DirectoryStream; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.attribute.BasicFileAttributes; -import java.nio.file.attribute.FileTime; -import java.nio.file.spi.FileSystemProvider; -import java.util.Arrays; -import java.util.Map; - -import javax.jcr.Property; -import javax.jcr.RepositoryException; -import javax.jcr.Session; -import javax.jcr.nodetype.NodeType; - -import org.apache.commons.io.IOUtils; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; -import org.apache.jackrabbit.core.RepositoryImpl; -import org.argeo.jackrabbit.fs.JackrabbitMemoryFsProvider; - -import junit.framework.TestCase; - -public class JcrFileSystemTest extends TestCase { - private final static Log log = LogFactory.getLog(JcrFileSystemTest.class); - - public void testMounts() throws Exception { - JackrabbitMemoryFsProvider fsProvider = new JackrabbitMemoryFsProvider() { - - @Override - protected void postRepositoryCreation(RepositoryImpl repositoryImpl) throws RepositoryException { - // create workspace - Session session = login(); - session.getWorkspace().createWorkspace("test"); - } - - }; - - Path rootPath = fsProvider.getPath(new URI("jcr+memory:/")); - log.debug("Got root " + rootPath); - Path testDir = rootPath.resolve("testDir"); - Files.createDirectory(testDir); - - Path testMount = fsProvider.getPath(new URI("jcr+memory:/test")); - log.debug("Test path"); - assertEquals(rootPath, testMount.getParent()); - assertEquals(testMount.getFileName(), rootPath.relativize(testMount)); - - Path testPath = testMount.resolve("test.txt"); - log.debug("Create file " + testPath); - Files.createFile(testPath); - BasicFileAttributes bfa = Files.readAttributes(testPath, BasicFileAttributes.class); - FileTime ft = bfa.creationTime(); - assertNotNull(ft); - assertTrue(bfa.isRegularFile()); - log.debug("Created " + testPath + " (" + ft + ")"); - Files.delete(testPath); - log.debug("Deleted " + testPath); - - // Browse directories from root - DirectoryStream files = Files.newDirectoryStream(rootPath); - int directoryCount = 0; - for (Path file : files) { - if (Files.isDirectory(file)) { - directoryCount++; - } - } - assertEquals(2, directoryCount); - - // Browse directories from mount - Path mountSubDir = testMount.resolve("mountSubDir"); - Files.createDirectory(mountSubDir); - Path otherSubDir = testMount.resolve("otherSubDir"); - Files.createDirectory(otherSubDir); - testPath = testMount.resolve("test.txt"); - Files.createFile(testPath); - files = Files.newDirectoryStream(testMount); - int fileCount = 0; - for (Path file : files) { - fileCount++; - } - assertEquals(3, fileCount); - - } - - public void testSimple() throws Exception { - FileSystemProvider fsProvider = new JackrabbitMemoryFsProvider(); - - // Simple file - Path rootPath = fsProvider.getPath(new URI("jcr+memory:/")); - log.debug("Got root " + rootPath); - Path testPath = fsProvider.getPath(new URI("jcr+memory:/test.txt")); - log.debug("Test path"); - assertEquals("test.txt", testPath.getFileName().toString()); - assertEquals(rootPath, testPath.getParent()); - assertEquals(testPath.getFileName(), rootPath.relativize(testPath)); - // relativize self should be empty path - Path selfRelative = testPath.relativize(testPath); - assertEquals("", selfRelative.toString()); - - log.debug("Create file " + testPath); - Files.createFile(testPath); - BasicFileAttributes bfa = Files.readAttributes(testPath, BasicFileAttributes.class); - FileTime ft = bfa.creationTime(); - assertNotNull(ft); - assertTrue(bfa.isRegularFile()); - log.debug("Created " + testPath + " (" + ft + ")"); - Files.delete(testPath); - log.debug("Deleted " + testPath); - String txt = "TEST\nTEST2\n"; - byte[] arr = txt.getBytes(); - Files.write(testPath, arr); - log.debug("Wrote " + testPath); - byte[] read = Files.readAllBytes(testPath); - assertTrue(Arrays.equals(arr, read)); - assertEquals(txt, new String(read)); - log.debug("Read " + testPath); - Path testDir = rootPath.resolve("testDir"); - log.debug("Resolved " + testDir); - // Copy - Files.createDirectory(testDir); - log.debug("Created directory " + testDir); - Path subsubdir = Files.createDirectories(testDir.resolve("subdir/subsubdir")); - log.debug("Created sub directories " + subsubdir); - Path copiedFile = testDir.resolve("copiedFile.txt"); - log.debug("Resolved " + copiedFile); - Path relativeCopiedFile = testDir.relativize(copiedFile); - assertEquals(copiedFile.getFileName().toString(), relativeCopiedFile.toString()); - log.debug("Relative copied file " + relativeCopiedFile); - try (OutputStream out = Files.newOutputStream(copiedFile); InputStream in = Files.newInputStream(testPath)) { - IOUtils.copy(in, out); - } - log.debug("Copied " + testPath + " to " + copiedFile); - Files.delete(testPath); - log.debug("Deleted " + testPath); - byte[] copiedRead = Files.readAllBytes(copiedFile); - assertTrue(Arrays.equals(copiedRead, read)); - log.debug("Read " + copiedFile); - // Browse directories - DirectoryStream files = Files.newDirectoryStream(testDir); - int fileCount = 0; - Path listedFile = null; - for (Path file : files) { - fileCount++; - if (!Files.isDirectory(file)) - listedFile = file; - } - assertEquals(2, fileCount); - assertEquals(copiedFile, listedFile); - assertEquals(copiedFile.toString(), listedFile.toString()); - log.debug("Listed " + testDir); - // Generic attributes - Map attrs = Files.readAttributes(copiedFile, "*"); - assertEquals(3, attrs.size()); - log.debug("Read attributes of " + copiedFile + ": " + attrs.keySet()); - // Direct node access - NodeFileAttributes nfa = Files.readAttributes(copiedFile, NodeFileAttributes.class); - nfa.getNode().addMixin(NodeType.MIX_LANGUAGE); - nfa.getNode().getSession().save(); - log.debug("Add mix:language"); - Files.setAttribute(copiedFile, Property.JCR_LANGUAGE, "fr"); - log.debug("Set language"); - attrs = Files.readAttributes(copiedFile, "*"); - assertEquals(4, attrs.size()); - log.debug("Read attributes of " + copiedFile + ": " + attrs.keySet()); - } - - public void testIllegalCharacters() throws Exception { - FileSystemProvider fsProvider = new JackrabbitMemoryFsProvider(); - String fileName = "tüßçt[1].txt"; - String pathStr = "/testDir/" + fileName; - Path testDir = fsProvider.getPath(new URI("jcr+memory:/testDir")); - Files.createDirectory(testDir); - Path testPath = testDir.resolve(fileName); - assertEquals(pathStr, testPath.toString()); - Files.createFile(testPath); - DirectoryStream files = Files.newDirectoryStream(testDir); - Path listedPath = files.iterator().next(); - assertEquals(pathStr, listedPath.toString()); - - String dirName = "*[~WeirdDir~]*"; - Path subDir = testDir.resolve(dirName); - Files.createDirectory(subDir); - subDir = testDir.resolve(dirName); - assertEquals(dirName, subDir.getFileName().toString()); - } -} diff --git a/org.argeo.core/pom.xml b/org.argeo.core/pom.xml index 37d89574d..ad5afd468 100644 --- a/org.argeo.core/pom.xml +++ b/org.argeo.core/pom.xml @@ -15,10 +15,10 @@ org.argeo.enterprise 2.3-SNAPSHOT - - org.argeo.commons - org.argeo.jcr - 2.3-SNAPSHOT - + + + + + \ No newline at end of file diff --git a/org.argeo.core/src/org/argeo/cli/fs/PathSync.java b/org.argeo.core/src/org/argeo/cli/fs/PathSync.java index 902318c44..9ab9cafad 100644 --- a/org.argeo.core/src/org/argeo/cli/fs/PathSync.java +++ b/org.argeo.core/src/org/argeo/cli/fs/PathSync.java @@ -8,7 +8,6 @@ import java.nio.file.Paths; import java.nio.file.spi.FileSystemProvider; import java.util.concurrent.Callable; -import org.argeo.jackrabbit.fs.DavexFsProvider; import org.argeo.sync.SyncResult; /** Synchronises two paths. */ @@ -49,8 +48,9 @@ public class PathSync implements Callable> { FileSystemProvider fsProvider = FileSystems.getDefault().provider(); path = fsProvider.getPath(uri); } else if (uri.getScheme().equals("davex")) { - FileSystemProvider fsProvider = new DavexFsProvider(); - path = fsProvider.getPath(uri); + 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(); diff --git a/org.argeo.core/src/org/argeo/cli/jcr/JcrCommands.java b/org.argeo.core/src/org/argeo/cli/jcr/JcrCommands.java deleted file mode 100644 index ea7467462..000000000 --- a/org.argeo.core/src/org/argeo/cli/jcr/JcrCommands.java +++ /dev/null @@ -1,18 +0,0 @@ -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.core/src/org/argeo/cli/jcr/JcrSync.java b/org.argeo.core/src/org/argeo/cli/jcr/JcrSync.java deleted file mode 100644 index 401f447c9..000000000 --- a/org.argeo.core/src/org/argeo/cli/jcr/JcrSync.java +++ /dev/null @@ -1,133 +0,0 @@ -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.core/src/org/argeo/cli/jcr/package-info.java b/org.argeo.core/src/org/argeo/cli/jcr/package-info.java deleted file mode 100644 index 6f3f01f3a..000000000 --- a/org.argeo.core/src/org/argeo/cli/jcr/package-info.java +++ /dev/null @@ -1,2 +0,0 @@ -/** JCR CLI commands. */ -package org.argeo.cli.jcr; \ No newline at end of file diff --git a/org.argeo.core/src/org/argeo/cli/jcr/repository-localfs.xml b/org.argeo.core/src/org/argeo/cli/jcr/repository-localfs.xml deleted file mode 100644 index 5e7759cf4..000000000 --- a/org.argeo.core/src/org/argeo/cli/jcr/repository-localfs.xml +++ /dev/null @@ -1,76 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/org.argeo.core/src/org/argeo/jackrabbit/JackrabbitAdminLoginModule.java b/org.argeo.core/src/org/argeo/jackrabbit/JackrabbitAdminLoginModule.java deleted file mode 100644 index 7396c87e7..000000000 --- a/org.argeo.core/src/org/argeo/jackrabbit/JackrabbitAdminLoginModule.java +++ /dev/null @@ -1,48 +0,0 @@ -package org.argeo.jackrabbit; - -import java.util.Map; - -import javax.security.auth.Subject; -import javax.security.auth.callback.CallbackHandler; -import javax.security.auth.login.LoginException; -import javax.security.auth.spi.LoginModule; - -import org.apache.jackrabbit.core.security.SecurityConstants; -import org.apache.jackrabbit.core.security.principal.AdminPrincipal; - -@Deprecated -public class JackrabbitAdminLoginModule implements LoginModule { - private Subject subject; - - @Override - public void initialize(Subject subject, CallbackHandler callbackHandler, - Map sharedState, Map options) { - this.subject = subject; - } - - @Override - public boolean login() throws LoginException { - // TODO check permission? - return true; - } - - @Override - public boolean commit() throws LoginException { - subject.getPrincipals().add( - new AdminPrincipal(SecurityConstants.ADMIN_ID)); - return true; - } - - @Override - public boolean abort() throws LoginException { - return true; - } - - @Override - public boolean logout() throws LoginException { - subject.getPrincipals().removeAll( - subject.getPrincipals(AdminPrincipal.class)); - return true; - } - -} diff --git a/org.argeo.core/src/org/argeo/jackrabbit/JackrabbitDataModelMigration.java b/org.argeo.core/src/org/argeo/jackrabbit/JackrabbitDataModelMigration.java deleted file mode 100644 index 9a49a063f..000000000 --- a/org.argeo.core/src/org/argeo/jackrabbit/JackrabbitDataModelMigration.java +++ /dev/null @@ -1,174 +0,0 @@ -package org.argeo.jackrabbit; - -import java.awt.geom.CubicCurve2D; -import java.io.IOException; -import java.io.InputStreamReader; -import java.io.Reader; -import java.net.URL; - -import javax.jcr.RepositoryException; -import javax.jcr.Session; - -import org.apache.commons.io.IOUtils; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; -import org.apache.jackrabbit.commons.cnd.CndImporter; -import org.apache.jackrabbit.commons.cnd.ParseException; -import org.apache.jackrabbit.core.config.RepositoryConfig; -import org.apache.jackrabbit.core.fs.FileSystemException; -import org.argeo.jcr.JcrCallback; -import org.argeo.jcr.JcrException; -import org.argeo.jcr.JcrUtils; - -/** Migrate the data in a Jackrabbit repository. */ -@Deprecated -public class JackrabbitDataModelMigration implements Comparable { - private final static Log log = LogFactory.getLog(JackrabbitDataModelMigration.class); - - private String dataModelNodePath; - private String targetVersion; - private URL migrationCnd; - private JcrCallback dataModification; - - /** - * Expects an already started repository with the old data model to migrate. - * Expects to be run with admin rights (Repository.login() will be used). - * - * @return true if a migration was performed and the repository needs to be - * restarted and its caches cleared. - */ - public Boolean migrate(Session session) { - long begin = System.currentTimeMillis(); - Reader reader = null; - try { - // check if already migrated - if (!session.itemExists(dataModelNodePath)) { -// log.warn("Node " + dataModelNodePath + " does not exist: nothing to migrate."); - return false; - } -// Node dataModelNode = session.getNode(dataModelNodePath); -// if (dataModelNode.hasProperty(ArgeoNames.ARGEO_DATA_MODEL_VERSION)) { -// String currentVersion = dataModelNode.getProperty( -// ArgeoNames.ARGEO_DATA_MODEL_VERSION).getString(); -// if (compareVersions(currentVersion, targetVersion) >= 0) { -// log.info("Data model at version " + currentVersion -// + ", no need to migrate."); -// return false; -// } -// } - - // apply transitional CND - if (migrationCnd != null) { - reader = new InputStreamReader(migrationCnd.openStream()); - CndImporter.registerNodeTypes(reader, session, true); - session.save(); -// log.info("Registered migration node types from " + migrationCnd); - } - - // modify data - dataModification.execute(session); - - // apply changes - session.save(); - - long duration = System.currentTimeMillis() - begin; -// log.info("Migration of data model " + dataModelNodePath + " to " + targetVersion + " performed in " -// + duration + "ms"); - return true; - } catch (RepositoryException e) { - JcrUtils.discardQuietly(session); - throw new JcrException("Migration of data model " + dataModelNodePath + " to " + targetVersion + " failed.", - e); - } catch (ParseException | IOException e) { - JcrUtils.discardQuietly(session); - throw new RuntimeException( - "Migration of data model " + dataModelNodePath + " to " + targetVersion + " failed.", e); - } finally { - JcrUtils.logoutQuietly(session); - IOUtils.closeQuietly(reader); - } - } - - protected static int compareVersions(String version1, String version2) { - // TODO do a proper version analysis and comparison - return version1.compareTo(version2); - } - - /** To be called on a stopped repository. */ - public static void clearRepositoryCaches(RepositoryConfig repositoryConfig) { - try { - String customeNodeTypesPath = "/nodetypes/custom_nodetypes.xml"; - // FIXME causes weird error in Eclipse - repositoryConfig.getFileSystem().deleteFile(customeNodeTypesPath); - if (log.isDebugEnabled()) - log.debug("Cleared " + customeNodeTypesPath); - } catch (RuntimeException e) { - throw e; - } catch (RepositoryException e) { - throw new JcrException(e); - } catch (FileSystemException e) { - throw new RuntimeException("Cannot clear node types cache.",e); - } - - // File customNodeTypes = new File(home.getPath() - // + "/repository/nodetypes/custom_nodetypes.xml"); - // if (customNodeTypes.exists()) { - // customNodeTypes.delete(); - // if (log.isDebugEnabled()) - // log.debug("Cleared " + customNodeTypes); - // } else { - // log.warn("File " + customNodeTypes + " not found."); - // } - } - - /* - * FOR USE IN (SORTED) SETS - */ - - public int compareTo(JackrabbitDataModelMigration dataModelMigration) { - // TODO make ordering smarter - if (dataModelNodePath.equals(dataModelMigration.dataModelNodePath)) - return compareVersions(targetVersion, dataModelMigration.targetVersion); - else - return dataModelNodePath.compareTo(dataModelMigration.dataModelNodePath); - } - - @Override - public boolean equals(Object obj) { - if (!(obj instanceof JackrabbitDataModelMigration)) - return false; - JackrabbitDataModelMigration dataModelMigration = (JackrabbitDataModelMigration) obj; - return dataModelNodePath.equals(dataModelMigration.dataModelNodePath) - && targetVersion.equals(dataModelMigration.targetVersion); - } - - @Override - public int hashCode() { - return targetVersion.hashCode(); - } - - public void setDataModelNodePath(String dataModelNodePath) { - this.dataModelNodePath = dataModelNodePath; - } - - public void setTargetVersion(String targetVersion) { - this.targetVersion = targetVersion; - } - - public void setMigrationCnd(URL migrationCnd) { - this.migrationCnd = migrationCnd; - } - - public void setDataModification(JcrCallback dataModification) { - this.dataModification = dataModification; - } - - public String getDataModelNodePath() { - return dataModelNodePath; - } - - public String getTargetVersion() { - return targetVersion; - } - -} diff --git a/org.argeo.core/src/org/argeo/jackrabbit/client/ClientDavexRepositoryFactory.java b/org.argeo.core/src/org/argeo/jackrabbit/client/ClientDavexRepositoryFactory.java deleted file mode 100644 index 77ad527e1..000000000 --- a/org.argeo.core/src/org/argeo/jackrabbit/client/ClientDavexRepositoryFactory.java +++ /dev/null @@ -1,26 +0,0 @@ -package org.argeo.jackrabbit.client; - -import java.util.Map; - -import javax.jcr.Repository; -import javax.jcr.RepositoryException; -import javax.jcr.RepositoryFactory; - -import org.apache.jackrabbit.jcr2spi.Jcr2spiRepositoryFactory; -import org.apache.jackrabbit.jcr2spi.RepositoryImpl; -import org.apache.jackrabbit.spi.RepositoryServiceFactory; - -/** A customised {@link RepositoryFactory} access a remote DAVEX service. */ -public class ClientDavexRepositoryFactory implements RepositoryFactory { - public final static String JACKRABBIT_DAVEX_URI = ClientDavexRepositoryServiceFactory.PARAM_REPOSITORY_URI; - public final static String JACKRABBIT_REMOTE_DEFAULT_WORKSPACE = ClientDavexRepositoryServiceFactory.PARAM_WORKSPACE_NAME_DEFAULT; - - @SuppressWarnings("rawtypes") - @Override - public Repository getRepository(Map parameters) throws RepositoryException { - RepositoryServiceFactory repositoryServiceFactory = new ClientDavexRepositoryServiceFactory(); - return RepositoryImpl - .create(new Jcr2spiRepositoryFactory.RepositoryConfigImpl(repositoryServiceFactory, parameters)); - } - -} diff --git a/org.argeo.core/src/org/argeo/jackrabbit/client/ClientDavexRepositoryService.java b/org.argeo.core/src/org/argeo/jackrabbit/client/ClientDavexRepositoryService.java deleted file mode 100644 index 0f9db8772..000000000 --- a/org.argeo.core/src/org/argeo/jackrabbit/client/ClientDavexRepositoryService.java +++ /dev/null @@ -1,40 +0,0 @@ -package org.argeo.jackrabbit.client; - -import javax.jcr.RepositoryException; - -import org.apache.http.client.protocol.HttpClientContext; -import org.apache.http.protocol.HttpContext; -import org.apache.jackrabbit.spi.SessionInfo; -import org.apache.jackrabbit.spi2davex.BatchReadConfig; -import org.apache.jackrabbit.spi2davex.RepositoryServiceImpl; - -/** - * Wrapper for {@link RepositoryServiceImpl} in order to access the underlying - * {@link HttpClientContext}. - */ -public class ClientDavexRepositoryService extends RepositoryServiceImpl { - - public ClientDavexRepositoryService(String jcrServerURI, BatchReadConfig batchReadConfig) - throws RepositoryException { - super(jcrServerURI, batchReadConfig); - } - - public ClientDavexRepositoryService(String jcrServerURI, String defaultWorkspaceName, - BatchReadConfig batchReadConfig, int itemInfoCacheSize, int maximumHttpConnections) - throws RepositoryException { - super(jcrServerURI, defaultWorkspaceName, batchReadConfig, itemInfoCacheSize, maximumHttpConnections); - } - - public ClientDavexRepositoryService(String jcrServerURI, String defaultWorkspaceName, - BatchReadConfig batchReadConfig, int itemInfoCacheSize) throws RepositoryException { - super(jcrServerURI, defaultWorkspaceName, batchReadConfig, itemInfoCacheSize); - } - - @Override - protected HttpContext getContext(SessionInfo sessionInfo) throws RepositoryException { - HttpClientContext result = HttpClientContext.create(); - result.setAuthCache(new NonSerialBasicAuthCache()); - return result; - } - -} diff --git a/org.argeo.core/src/org/argeo/jackrabbit/client/ClientDavexRepositoryServiceFactory.java b/org.argeo.core/src/org/argeo/jackrabbit/client/ClientDavexRepositoryServiceFactory.java deleted file mode 100644 index 4b240f060..000000000 --- a/org.argeo.core/src/org/argeo/jackrabbit/client/ClientDavexRepositoryServiceFactory.java +++ /dev/null @@ -1,82 +0,0 @@ -package org.argeo.jackrabbit.client; - -import java.util.Map; - -import javax.jcr.RepositoryException; - -import org.apache.jackrabbit.spi.RepositoryService; -import org.apache.jackrabbit.spi.commons.ItemInfoCacheImpl; -import org.apache.jackrabbit.spi2davex.BatchReadConfig; -import org.apache.jackrabbit.spi2davex.Spi2davexRepositoryServiceFactory; - -/** - * Wrapper for {@link Spi2davexRepositoryServiceFactory} in order to create a - * {@link ClientDavexRepositoryService}. - */ -public class ClientDavexRepositoryServiceFactory extends Spi2davexRepositoryServiceFactory { - @Override - public RepositoryService createRepositoryService(Map parameters) throws RepositoryException { - // retrieve the repository uri - String uri; - if (parameters == null) { - uri = System.getProperty(PARAM_REPOSITORY_URI); - } else { - Object repoUri = parameters.get(PARAM_REPOSITORY_URI); - uri = (repoUri == null) ? null : repoUri.toString(); - } - if (uri == null) { - uri = DEFAULT_REPOSITORY_URI; - } - - // load other optional configuration parameters - BatchReadConfig brc = null; - int itemInfoCacheSize = ItemInfoCacheImpl.DEFAULT_CACHE_SIZE; - int maximumHttpConnections = 0; - - // since JCR-4120 the default workspace name is no longer set to 'default' - // note: if running with JCR Server < 1.5 a default workspace name must - // therefore be configured - String workspaceNameDefault = null; - - if (parameters != null) { - // batchRead config - Object param = parameters.get(PARAM_BATCHREAD_CONFIG); - if (param != null && param instanceof BatchReadConfig) { - brc = (BatchReadConfig) param; - } - - // itemCache size config - param = parameters.get(PARAM_ITEMINFO_CACHE_SIZE); - if (param != null) { - try { - itemInfoCacheSize = Integer.parseInt(param.toString()); - } catch (NumberFormatException e) { - // ignore, use default - } - } - - // max connections config - param = parameters.get(PARAM_MAX_CONNECTIONS); - if (param != null) { - try { - maximumHttpConnections = Integer.parseInt(param.toString()); - } catch (NumberFormatException e) { - // using default - } - } - - param = parameters.get(PARAM_WORKSPACE_NAME_DEFAULT); - if (param != null) { - workspaceNameDefault = param.toString(); - } - } - - if (maximumHttpConnections > 0) { - return new ClientDavexRepositoryService(uri, workspaceNameDefault, brc, itemInfoCacheSize, - maximumHttpConnections); - } else { - return new ClientDavexRepositoryService(uri, workspaceNameDefault, brc, itemInfoCacheSize); - } - } - -} diff --git a/org.argeo.core/src/org/argeo/jackrabbit/client/JackrabbitClient.java b/org.argeo.core/src/org/argeo/jackrabbit/client/JackrabbitClient.java deleted file mode 100644 index e08f4d6c7..000000000 --- a/org.argeo.core/src/org/argeo/jackrabbit/client/JackrabbitClient.java +++ /dev/null @@ -1,125 +0,0 @@ -package org.argeo.jackrabbit.client; - -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStreamReader; -import java.net.URI; -import java.net.URISyntaxException; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.HashMap; -import java.util.Map; - -import javax.jcr.Node; -import javax.jcr.NodeIterator; -import javax.jcr.Repository; -import javax.jcr.RepositoryException; -import javax.jcr.RepositoryFactory; -import javax.jcr.Session; - -import org.apache.http.client.protocol.HttpClientContext; -import org.apache.http.protocol.HttpContext; -import org.apache.jackrabbit.jcr2dav.Jcr2davRepositoryFactory; -import org.apache.jackrabbit.jcr2spi.Jcr2spiRepositoryFactory; -import org.apache.jackrabbit.jcr2spi.RepositoryImpl; -import org.apache.jackrabbit.spi.RepositoryService; -import org.apache.jackrabbit.spi.RepositoryServiceFactory; -import org.apache.jackrabbit.spi.SessionInfo; -import org.apache.jackrabbit.spi.commons.ItemInfoCacheImpl; -import org.apache.jackrabbit.spi2davex.BatchReadConfig; -import org.apache.jackrabbit.spi2davex.RepositoryServiceImpl; -import org.apache.jackrabbit.spi2davex.Spi2davexRepositoryServiceFactory; -import org.argeo.jcr.JcrUtils; - -/** Minimal client to test JCR DAVEX connectivity. */ -public class JackrabbitClient { - final static String JACKRABBIT_REPOSITORY_URI = "org.apache.jackrabbit.repository.uri"; - final static String JACKRABBIT_DAVEX_URI = "org.apache.jackrabbit.spi2davex.uri"; - final static String JACKRABBIT_REMOTE_DEFAULT_WORKSPACE = "org.apache.jackrabbit.spi2davex.WorkspaceNameDefault"; - - public static void main(String[] args) { - String repoUri = args.length == 0 ? "http://root:demo@localhost:7070/jcr/ego" : args[0]; - String workspace = args.length < 2 ? "home" : args[1]; - - Repository repository = null; - Session session = null; - - URI uri; - try { - uri = new URI(repoUri); - } catch (URISyntaxException e1) { - throw new IllegalArgumentException(e1); - } - - if (uri.getScheme().equals("http") || uri.getScheme().equals("https")) { - - RepositoryFactory repositoryFactory = new Jcr2davRepositoryFactory() { - @SuppressWarnings("rawtypes") - public Repository getRepository(Map parameters) throws RepositoryException { - RepositoryServiceFactory repositoryServiceFactory = new Spi2davexRepositoryServiceFactory() { - - @Override - public RepositoryService createRepositoryService(Map parameters) - throws RepositoryException { - Object uri = parameters.get(JACKRABBIT_DAVEX_URI); - Object defaultWorkspace = parameters.get(JACKRABBIT_REMOTE_DEFAULT_WORKSPACE); - BatchReadConfig brc = null; - return new RepositoryServiceImpl(uri.toString(), defaultWorkspace.toString(), brc, - ItemInfoCacheImpl.DEFAULT_CACHE_SIZE) { - - @Override - protected HttpContext getContext(SessionInfo sessionInfo) throws RepositoryException { - HttpClientContext result = HttpClientContext.create(); - result.setAuthCache(new NonSerialBasicAuthCache()); - return result; - } - - }; - } - }; - return RepositoryImpl.create( - new Jcr2spiRepositoryFactory.RepositoryConfigImpl(repositoryServiceFactory, parameters)); - } - }; - Map params = new HashMap(); - params.put(JACKRABBIT_DAVEX_URI, repoUri.toString()); - // FIXME make it configurable - params.put(JACKRABBIT_REMOTE_DEFAULT_WORKSPACE, "sys"); - - try { - repository = repositoryFactory.getRepository(params); - if (repository != null) - session = repository.login(workspace); - else - throw new IllegalArgumentException("Repository " + repoUri + " not found"); - } catch (RepositoryException e) { - e.printStackTrace(); - } - - } else { - Path path = Paths.get(uri.getPath()); - } - - try { - Node rootNode = session.getRootNode(); - NodeIterator nit = rootNode.getNodes(); - while (nit.hasNext()) { - System.out.println(nit.nextNode().getPath()); - } - - Node newNode = JcrUtils.mkdirs(rootNode, "dir/subdir"); - System.out.println("Created folder " + newNode.getPath()); - Node newFile = JcrUtils.copyBytesAsFile(newNode, "test.txt", "TEST".getBytes()); - System.out.println("Created file " + newFile.getPath()); - try (BufferedReader reader = new BufferedReader(new InputStreamReader(JcrUtils.getFileAsStream(newFile)))) { - System.out.println("Read " + reader.readLine()); - } catch (IOException e) { - e.printStackTrace(); - } - newNode.getParent().remove(); - System.out.println("Removed new nodes"); - } catch (RepositoryException e) { - e.printStackTrace(); - } - } -} diff --git a/org.argeo.core/src/org/argeo/jackrabbit/client/NonSerialBasicAuthCache.java b/org.argeo.core/src/org/argeo/jackrabbit/client/NonSerialBasicAuthCache.java deleted file mode 100644 index 3fb0db9a0..000000000 --- a/org.argeo.core/src/org/argeo/jackrabbit/client/NonSerialBasicAuthCache.java +++ /dev/null @@ -1,41 +0,0 @@ -package org.argeo.jackrabbit.client; - -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; - -import org.apache.http.HttpHost; -import org.apache.http.auth.AuthScheme; -import org.apache.http.client.AuthCache; - -/** - * Implementation of {@link AuthCache} which doesn't use serialization, as it is - * not supported by GraalVM at this stage. - */ -public class NonSerialBasicAuthCache implements AuthCache { - private final Map cache; - - public NonSerialBasicAuthCache() { - cache = new ConcurrentHashMap(); - } - - @Override - public void put(HttpHost host, AuthScheme authScheme) { - cache.put(host, authScheme); - } - - @Override - public AuthScheme get(HttpHost host) { - return cache.get(host); - } - - @Override - public void remove(HttpHost host) { - cache.remove(host); - } - - @Override - public void clear() { - cache.clear(); - } - -} diff --git a/org.argeo.core/src/org/argeo/jackrabbit/fs/AbstractJackrabbitFsProvider.java b/org.argeo.core/src/org/argeo/jackrabbit/fs/AbstractJackrabbitFsProvider.java deleted file mode 100644 index a2eb98302..000000000 --- a/org.argeo.core/src/org/argeo/jackrabbit/fs/AbstractJackrabbitFsProvider.java +++ /dev/null @@ -1,7 +0,0 @@ -package org.argeo.jackrabbit.fs; - -import org.argeo.jcr.fs.JcrFileSystemProvider; - -public abstract class AbstractJackrabbitFsProvider extends JcrFileSystemProvider { - -} diff --git a/org.argeo.core/src/org/argeo/jackrabbit/fs/DavexFsProvider.java b/org.argeo.core/src/org/argeo/jackrabbit/fs/DavexFsProvider.java deleted file mode 100644 index 1cae6e493..000000000 --- a/org.argeo.core/src/org/argeo/jackrabbit/fs/DavexFsProvider.java +++ /dev/null @@ -1,149 +0,0 @@ -package org.argeo.jackrabbit.fs; - -import java.io.IOException; -import java.net.URI; -import java.net.URISyntaxException; -import java.nio.file.DirectoryStream; -import java.nio.file.FileSystem; -import java.nio.file.FileSystemAlreadyExistsException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.HashMap; -import java.util.Map; - -import javax.jcr.Repository; -import javax.jcr.RepositoryFactory; -import javax.jcr.Session; - -import org.argeo.jackrabbit.client.ClientDavexRepositoryFactory; -import org.argeo.jcr.fs.JcrFileSystem; -import org.argeo.jcr.fs.JcrFsException; - -/** - * A file system provider based on a JCR repository remotely accessed via the - * DAVEX protocol. - */ -public class DavexFsProvider extends AbstractJackrabbitFsProvider { - final static String DEFAULT_JACKRABBIT_REMOTE_DEFAULT_WORKSPACE = "sys"; - - private Map fileSystems = new HashMap<>(); - - @Override - public String getScheme() { - return "davex"; - } - - @Override - public FileSystem newFileSystem(URI uri, Map env) throws IOException { - if (uri.getHost() == null) - throw new IllegalArgumentException("An host should be provided"); - try { - URI repoUri = new URI("http", uri.getUserInfo(), uri.getHost(), uri.getPort(), uri.getPath(), null, null); - String repoKey = repoUri.toString(); - if (fileSystems.containsKey(repoKey)) - throw new FileSystemAlreadyExistsException("CMS file system already exists for " + repoKey); - RepositoryFactory repositoryFactory = new ClientDavexRepositoryFactory(); - return tryGetRepo(repositoryFactory, repoUri, "home"); - } catch (URISyntaxException e) { - throw new IllegalArgumentException("Cannot open file system " + uri, e); - } - } - - private JcrFileSystem tryGetRepo(RepositoryFactory repositoryFactory, URI repoUri, String workspace) - throws IOException { - Map params = new HashMap(); - params.put(ClientDavexRepositoryFactory.JACKRABBIT_DAVEX_URI, repoUri.toString()); - // TODO better integrate with OSGi or other configuration than system - // properties. - String remoteDefaultWorkspace = System.getProperty( - ClientDavexRepositoryFactory.JACKRABBIT_REMOTE_DEFAULT_WORKSPACE, - DEFAULT_JACKRABBIT_REMOTE_DEFAULT_WORKSPACE); - params.put(ClientDavexRepositoryFactory.JACKRABBIT_REMOTE_DEFAULT_WORKSPACE, remoteDefaultWorkspace); - Repository repository = null; - Session session = null; - try { - repository = repositoryFactory.getRepository(params); - if (repository != null) - session = repository.login(workspace); - } catch (Exception e) { - // silent - } - - if (session == null) { - if (repoUri.getPath() == null || repoUri.getPath().equals("/")) - return null; - String repoUriStr = repoUri.toString(); - if (repoUriStr.endsWith("/")) - repoUriStr = repoUriStr.substring(0, repoUriStr.length() - 1); - String nextRepoUriStr = repoUriStr.substring(0, repoUriStr.lastIndexOf('/')); - String nextWorkspace = repoUriStr.substring(repoUriStr.lastIndexOf('/') + 1); - URI nextUri; - try { - nextUri = new URI(nextRepoUriStr); - } catch (URISyntaxException e) { - throw new IllegalArgumentException("Badly formatted URI", e); - } - return tryGetRepo(repositoryFactory, nextUri, nextWorkspace); - } else { - JcrFileSystem fileSystem = new JcrFileSystem(this, repository); - fileSystems.put(repoUri.toString() + "/" + workspace, fileSystem); - return fileSystem; - } - } - - @Override - public FileSystem getFileSystem(URI uri) { - return currentUserFileSystem(uri); - } - - @Override - public Path getPath(URI uri) { - JcrFileSystem fileSystem = currentUserFileSystem(uri); - if (fileSystem == null) - try { - fileSystem = (JcrFileSystem) newFileSystem(uri, new HashMap()); - if (fileSystem == null) - throw new IllegalArgumentException("No file system found for " + uri); - } catch (IOException e) { - throw new JcrFsException("Could not autocreate file system", e); - } - URI repoUri = null; - try { - repoUri = new URI("http", uri.getUserInfo(), uri.getHost(), uri.getPort(), uri.getPath(), null, null); - } catch (URISyntaxException e) { - throw new IllegalArgumentException(e); - } - String uriStr = repoUri.toString(); - String localPath = null; - for (String key : fileSystems.keySet()) { - if (uriStr.startsWith(key)) { - localPath = uriStr.toString().substring(key.length()); - } - } - if ("".equals(localPath)) - localPath = "/"; - return fileSystem.getPath(localPath); - } - - private JcrFileSystem currentUserFileSystem(URI uri) { - for (String key : fileSystems.keySet()) { - if (uri.toString().startsWith(key)) - return fileSystems.get(key); - } - return null; - } - - public static void main(String args[]) { - try { - DavexFsProvider fsProvider = new DavexFsProvider(); - Path path = fsProvider.getPath(new URI("davex://root:demo@localhost:7070/jcr/ego/")); - System.out.println(path); - DirectoryStream ds = Files.newDirectoryStream(path); - for (Path p : ds) { - System.out.println("- " + p); - } - } catch (Exception e) { - e.printStackTrace(); - } - } -} diff --git a/org.argeo.core/src/org/argeo/jackrabbit/fs/JackrabbitMemoryFsProvider.java b/org.argeo.core/src/org/argeo/jackrabbit/fs/JackrabbitMemoryFsProvider.java deleted file mode 100644 index e3a70d084..000000000 --- a/org.argeo.core/src/org/argeo/jackrabbit/fs/JackrabbitMemoryFsProvider.java +++ /dev/null @@ -1,87 +0,0 @@ -package org.argeo.jackrabbit.fs; - -import java.io.IOException; -import java.net.URI; -import java.net.URISyntaxException; -import java.net.URL; -import java.nio.file.FileSystem; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.HashMap; -import java.util.Map; - -import javax.jcr.Credentials; -import javax.jcr.Repository; -import javax.jcr.RepositoryException; -import javax.jcr.Session; -import javax.jcr.SimpleCredentials; - -import org.apache.jackrabbit.core.RepositoryImpl; -import org.apache.jackrabbit.core.config.RepositoryConfig; -import org.argeo.jcr.fs.JcrFileSystem; -import org.argeo.jcr.fs.JcrFsException; - -public class JackrabbitMemoryFsProvider extends AbstractJackrabbitFsProvider { - private RepositoryImpl repository; - private JcrFileSystem fileSystem; - - private Credentials credentials; - - public JackrabbitMemoryFsProvider() { - String username = System.getProperty("user.name"); - credentials = new SimpleCredentials(username, username.toCharArray()); - } - - @Override - public String getScheme() { - return "jcr+memory"; - } - - @Override - public FileSystem newFileSystem(URI uri, Map env) throws IOException { - try { - Path tempDir = Files.createTempDirectory("fs-memory"); - URL confUrl = JackrabbitMemoryFsProvider.class.getResource("fs-memory.xml"); - RepositoryConfig repositoryConfig = RepositoryConfig.create(confUrl.toURI(), tempDir.toString()); - repository = RepositoryImpl.create(repositoryConfig); - postRepositoryCreation(repository); - fileSystem = new JcrFileSystem(this, repository, credentials); - return fileSystem; - } catch (RepositoryException | URISyntaxException e) { - throw new IOException("Cannot login to repository", e); - } - } - - @Override - public FileSystem getFileSystem(URI uri) { - return fileSystem; - } - - @Override - public Path getPath(URI uri) { - String path = uri.getPath(); - if (fileSystem == null) - try { - newFileSystem(uri, new HashMap()); - } catch (IOException e) { - throw new JcrFsException("Could not autocreate file system", e); - } - return fileSystem.getPath(path); - } - - public Repository getRepository() { - return repository; - } - - public Session login() throws RepositoryException { - return getRepository().login(credentials); - } - - /** - * Called after the repository has been created and before the file system is - * created. - */ - protected void postRepositoryCreation(RepositoryImpl repositoryImpl) throws RepositoryException { - - } -} diff --git a/org.argeo.core/src/org/argeo/jackrabbit/fs/fs-memory.xml b/org.argeo.core/src/org/argeo/jackrabbit/fs/fs-memory.xml deleted file mode 100644 index f2541fb4e..000000000 --- a/org.argeo.core/src/org/argeo/jackrabbit/fs/fs-memory.xml +++ /dev/null @@ -1,57 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/org.argeo.core/src/org/argeo/jackrabbit/fs/package-info.java b/org.argeo.core/src/org/argeo/jackrabbit/fs/package-info.java deleted file mode 100644 index c9ec2c3b9..000000000 --- a/org.argeo.core/src/org/argeo/jackrabbit/fs/package-info.java +++ /dev/null @@ -1,2 +0,0 @@ -/** Java NIO file system implementation based on Jackrabbit. */ -package org.argeo.jackrabbit.fs; \ No newline at end of file diff --git a/org.argeo.core/src/org/argeo/jackrabbit/package-info.java b/org.argeo.core/src/org/argeo/jackrabbit/package-info.java deleted file mode 100644 index 17497d62c..000000000 --- a/org.argeo.core/src/org/argeo/jackrabbit/package-info.java +++ /dev/null @@ -1,2 +0,0 @@ -/** Generic Jackrabbit utilities. */ -package org.argeo.jackrabbit; \ No newline at end of file diff --git a/org.argeo.core/src/org/argeo/jackrabbit/repository-h2.xml b/org.argeo.core/src/org/argeo/jackrabbit/repository-h2.xml deleted file mode 100644 index 05267621f..000000000 --- a/org.argeo.core/src/org/argeo/jackrabbit/repository-h2.xml +++ /dev/null @@ -1,82 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/org.argeo.core/src/org/argeo/jackrabbit/repository-localfs.xml b/org.argeo.core/src/org/argeo/jackrabbit/repository-localfs.xml deleted file mode 100644 index 3d2470863..000000000 --- a/org.argeo.core/src/org/argeo/jackrabbit/repository-localfs.xml +++ /dev/null @@ -1,60 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/org.argeo.core/src/org/argeo/jackrabbit/repository-memory.xml b/org.argeo.core/src/org/argeo/jackrabbit/repository-memory.xml deleted file mode 100644 index ecee5bdad..000000000 --- a/org.argeo.core/src/org/argeo/jackrabbit/repository-memory.xml +++ /dev/null @@ -1,55 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/org.argeo.core/src/org/argeo/jackrabbit/repository-postgresql-ds.xml b/org.argeo.core/src/org/argeo/jackrabbit/repository-postgresql-ds.xml deleted file mode 100644 index 07a0d0428..000000000 --- a/org.argeo.core/src/org/argeo/jackrabbit/repository-postgresql-ds.xml +++ /dev/null @@ -1,82 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/org.argeo.core/src/org/argeo/jackrabbit/repository-postgresql.xml b/org.argeo.core/src/org/argeo/jackrabbit/repository-postgresql.xml deleted file mode 100644 index 967782820..000000000 --- a/org.argeo.core/src/org/argeo/jackrabbit/repository-postgresql.xml +++ /dev/null @@ -1,79 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/org.argeo.core/src/org/argeo/jackrabbit/security/JackrabbitSecurityUtils.java b/org.argeo.core/src/org/argeo/jackrabbit/security/JackrabbitSecurityUtils.java deleted file mode 100644 index a75c79541..000000000 --- a/org.argeo.core/src/org/argeo/jackrabbit/security/JackrabbitSecurityUtils.java +++ /dev/null @@ -1,80 +0,0 @@ -package org.argeo.jackrabbit.security; - -import java.security.Principal; -import java.util.ArrayList; -import java.util.List; - -import javax.jcr.RepositoryException; -import javax.jcr.Session; -import javax.jcr.security.Privilege; - -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; -import org.apache.jackrabbit.api.security.JackrabbitAccessControlList; -import org.apache.jackrabbit.api.security.JackrabbitAccessControlManager; -import org.argeo.jcr.JcrUtils; - -/** Utilities around Jackrabbit security extensions. */ -public class JackrabbitSecurityUtils { - private final static Log log = LogFactory.getLog(JackrabbitSecurityUtils.class); - - /** - * Convenience method for denying a single privilege to a principal (user or - * role), typically jcr:all - */ - public synchronized static void denyPrivilege(Session session, String path, String principal, String privilege) - throws RepositoryException { - List privileges = new ArrayList(); - privileges.add(session.getAccessControlManager().privilegeFromName(privilege)); - denyPrivileges(session, path, () -> principal, privileges); - } - - /** - * Deny privileges on a path to a {@link Principal}. The path must already - * exist. Session is saved. Synchronized to prevent concurrent modifications of - * the same node. - */ - public synchronized static Boolean denyPrivileges(Session session, String path, Principal principal, - List privs) throws RepositoryException { - // make sure the session is in line with the persisted state - session.refresh(false); - JackrabbitAccessControlManager acm = (JackrabbitAccessControlManager) session.getAccessControlManager(); - JackrabbitAccessControlList acl = (JackrabbitAccessControlList) JcrUtils.getAccessControlList(acm, path); - -// accessControlEntries: for (AccessControlEntry ace : acl.getAccessControlEntries()) { -// Principal currentPrincipal = ace.getPrincipal(); -// if (currentPrincipal.getName().equals(principal.getName())) { -// Privilege[] currentPrivileges = ace.getPrivileges(); -// if (currentPrivileges.length != privs.size()) -// break accessControlEntries; -// for (int i = 0; i < currentPrivileges.length; i++) { -// Privilege currP = currentPrivileges[i]; -// Privilege p = privs.get(i); -// if (!currP.getName().equals(p.getName())) { -// break accessControlEntries; -// } -// } -// return false; -// } -// } - - Privilege[] privileges = privs.toArray(new Privilege[privs.size()]); - acl.addEntry(principal, privileges, false); - acm.setPolicy(path, acl); - if (log.isDebugEnabled()) { - StringBuffer privBuf = new StringBuffer(); - for (Privilege priv : privs) - privBuf.append(priv.getName()); - log.debug("Denied privileges " + privBuf + " to " + principal.getName() + " on " + path + " in '" - + session.getWorkspace().getName() + "'"); - } - session.refresh(true); - session.save(); - return true; - } - - /** Singleton. */ - private JackrabbitSecurityUtils() { - - } -} diff --git a/org.argeo.core/src/org/argeo/jackrabbit/security/package-info.java b/org.argeo.core/src/org/argeo/jackrabbit/security/package-info.java deleted file mode 100644 index f3a282c4e..000000000 --- a/org.argeo.core/src/org/argeo/jackrabbit/security/package-info.java +++ /dev/null @@ -1,2 +0,0 @@ -/** Generic Jackrabbit security utilities. */ -package org.argeo.jackrabbit.security; \ No newline at end of file diff --git a/org.argeo.core/src/org/argeo/jackrabbit/unit/AbstractJackrabbitTestCase.java b/org.argeo.core/src/org/argeo/jackrabbit/unit/AbstractJackrabbitTestCase.java deleted file mode 100644 index f65432eb7..000000000 --- a/org.argeo.core/src/org/argeo/jackrabbit/unit/AbstractJackrabbitTestCase.java +++ /dev/null @@ -1,51 +0,0 @@ -package org.argeo.jackrabbit.unit; - -import java.net.URL; - -import javax.jcr.Repository; - -import org.apache.commons.io.FileUtils; -import org.apache.jackrabbit.core.RepositoryImpl; -import org.apache.jackrabbit.core.config.RepositoryConfig; -import org.argeo.jcr.unit.AbstractJcrTestCase; - -/** Factorizes configuration of an in memory transient repository */ -public abstract class AbstractJackrabbitTestCase extends AbstractJcrTestCase { - protected RepositoryImpl repositoryImpl; - - // protected File getRepositoryFile() throws Exception { - // Resource res = new ClassPathResource( - // "org/argeo/jackrabbit/unit/repository-memory.xml"); - // return res.getFile(); - // } - - public AbstractJackrabbitTestCase() { - URL url = AbstractJackrabbitTestCase.class.getResource("jaas.config"); - assert url != null; - System.setProperty("java.security.auth.login.config", url.toString()); - } - - protected Repository createRepository() throws Exception { - // Repository repository = new TransientRepository(getRepositoryFile(), - // getHomeDir()); - RepositoryConfig repositoryConfig = RepositoryConfig.create( - AbstractJackrabbitTestCase.class - .getResourceAsStream(getRepositoryConfigResource()), - getHomeDir().getAbsolutePath()); - RepositoryImpl repositoryImpl = RepositoryImpl.create(repositoryConfig); - return repositoryImpl; - } - - protected String getRepositoryConfigResource() { - return "repository-memory.xml"; - } - - @Override - protected void clearRepository(Repository repository) throws Exception { - RepositoryImpl repositoryImpl = (RepositoryImpl) repository; - if (repositoryImpl != null) - repositoryImpl.shutdown(); - FileUtils.deleteDirectory(getHomeDir()); - } - -} diff --git a/org.argeo.core/src/org/argeo/jackrabbit/unit/jaas.config b/org.argeo.core/src/org/argeo/jackrabbit/unit/jaas.config deleted file mode 100644 index 0313f91e5..000000000 --- a/org.argeo.core/src/org/argeo/jackrabbit/unit/jaas.config +++ /dev/null @@ -1,7 +0,0 @@ -TEST_JACKRABBIT_ADMIN { - org.argeo.cms.auth.DataAdminLoginModule requisite; -}; - -Jackrabbit { - org.argeo.security.jackrabbit.SystemJackrabbitLoginModule requisite; -}; diff --git a/org.argeo.core/src/org/argeo/jackrabbit/unit/package-info.java b/org.argeo.core/src/org/argeo/jackrabbit/unit/package-info.java deleted file mode 100644 index 3b6143b34..000000000 --- a/org.argeo.core/src/org/argeo/jackrabbit/unit/package-info.java +++ /dev/null @@ -1,2 +0,0 @@ -/** Helpers for unit tests with Jackrabbit repositories. */ -package org.argeo.jackrabbit.unit; \ No newline at end of file diff --git a/org.argeo.core/src/org/argeo/jackrabbit/unit/repository-h2.xml b/org.argeo.core/src/org/argeo/jackrabbit/unit/repository-h2.xml deleted file mode 100644 index 348dc288b..000000000 --- a/org.argeo.core/src/org/argeo/jackrabbit/unit/repository-h2.xml +++ /dev/null @@ -1,81 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/org.argeo.core/src/org/argeo/jackrabbit/unit/repository-memory.xml b/org.argeo.core/src/org/argeo/jackrabbit/unit/repository-memory.xml deleted file mode 100644 index 839542417..000000000 --- a/org.argeo.core/src/org/argeo/jackrabbit/unit/repository-memory.xml +++ /dev/null @@ -1,72 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/org.argeo.core/src/org/argeo/jcr/proxy/AbstractUrlProxy.java b/org.argeo.core/src/org/argeo/jcr/proxy/AbstractUrlProxy.java deleted file mode 100644 index 0984276dd..000000000 --- a/org.argeo.core/src/org/argeo/jcr/proxy/AbstractUrlProxy.java +++ /dev/null @@ -1,154 +0,0 @@ -package org.argeo.jcr.proxy; - -import java.io.IOException; -import java.io.InputStream; -import java.net.URL; - -import javax.jcr.Binary; -import javax.jcr.Node; -import javax.jcr.Property; -import javax.jcr.Repository; -import javax.jcr.RepositoryException; -import javax.jcr.Session; -import javax.jcr.nodetype.NodeType; - -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; -import org.argeo.jcr.JcrException; -import org.argeo.jcr.JcrUtils; - -/** Base class for URL based proxys. */ -public abstract class AbstractUrlProxy implements ResourceProxy { - private final static Log log = LogFactory.getLog(AbstractUrlProxy.class); - - private Repository jcrRepository; - private Session jcrAdminSession; - private String proxyWorkspace = "proxy"; - - protected abstract Node retrieve(Session session, String path); - - void init() { - try { - jcrAdminSession = JcrUtils.loginOrCreateWorkspace(jcrRepository, proxyWorkspace); - beforeInitSessionSave(jcrAdminSession); - if (jcrAdminSession.hasPendingChanges()) - jcrAdminSession.save(); - } catch (RepositoryException e) { - JcrUtils.discardQuietly(jcrAdminSession); - throw new JcrException("Cannot initialize URL proxy", e); - } - } - - /** - * Called before the (admin) session is saved at the end of the initialization. - * Does nothing by default, to be overridden. - */ - protected void beforeInitSessionSave(Session session) throws RepositoryException { - } - - void destroy() { - JcrUtils.logoutQuietly(jcrAdminSession); - } - - /** - * Called before the (admin) session is logged out when resources are released. - * Does nothing by default, to be overridden. - */ - protected void beforeDestroySessionLogout() throws RepositoryException { - } - - public Node proxy(String path) { - // we open a JCR session with client credentials in order not to use the - // admin session in multiple thread or make it a bottleneck. - Node nodeAdmin = null; - Node nodeClient = null; - Session clientSession = null; - try { - clientSession = jcrRepository.login(proxyWorkspace); - if (!clientSession.itemExists(path) || shouldUpdate(clientSession, path)) { - nodeAdmin = retrieveAndSave(path); - if (nodeAdmin != null) - nodeClient = clientSession.getNode(path); - } else - nodeClient = clientSession.getNode(path); - return nodeClient; - } catch (RepositoryException e) { - throw new JcrException("Cannot proxy " + path, e); - } finally { - if (nodeClient == null) - JcrUtils.logoutQuietly(clientSession); - } - } - - protected synchronized Node retrieveAndSave(String path) { - try { - Node node = retrieve(jcrAdminSession, path); - if (node == null) - return null; - jcrAdminSession.save(); - return node; - } catch (RepositoryException e) { - JcrUtils.discardQuietly(jcrAdminSession); - throw new JcrException("Cannot retrieve and save " + path, e); - } finally { - notifyAll(); - } - } - - /** Session is not saved */ - protected synchronized Node proxyUrl(Session session, String remoteUrl, String path) throws RepositoryException { - Node node = null; - if (session.itemExists(path)) { - // throw new ArgeoJcrException("Node " + path + " already exists"); - } - try (InputStream in = new URL(remoteUrl).openStream()) { - // URL u = new URL(remoteUrl); - // in = u.openStream(); - node = importFile(session, path, in); - } catch (IOException e) { - if (log.isDebugEnabled()) { - log.debug("Cannot read " + remoteUrl + ", skipping... " + e.getMessage()); - // log.trace("Cannot read because of ", e); - } - JcrUtils.discardQuietly(session); - // } finally { - // IOUtils.closeQuietly(in); - } - return node; - } - - protected synchronized Node importFile(Session session, String path, InputStream in) throws RepositoryException { - Binary binary = null; - try { - Node content = null; - Node node = null; - if (!session.itemExists(path)) { - node = JcrUtils.mkdirs(session, path, NodeType.NT_FILE, NodeType.NT_FOLDER, false); - content = node.addNode(Node.JCR_CONTENT, NodeType.NT_UNSTRUCTURED); - } else { - node = session.getNode(path); - content = node.getNode(Node.JCR_CONTENT); - } - binary = session.getValueFactory().createBinary(in); - content.setProperty(Property.JCR_DATA, binary); - JcrUtils.updateLastModifiedAndParents(node, null, true); - return node; - } finally { - JcrUtils.closeQuietly(binary); - } - } - - /** Whether the file should be updated. */ - protected Boolean shouldUpdate(Session clientSession, String nodePath) { - return false; - } - - public void setJcrRepository(Repository jcrRepository) { - this.jcrRepository = jcrRepository; - } - - public void setProxyWorkspace(String localWorkspace) { - this.proxyWorkspace = localWorkspace; - } - -} diff --git a/org.argeo.core/src/org/argeo/jcr/proxy/ResourceProxy.java b/org.argeo.core/src/org/argeo/jcr/proxy/ResourceProxy.java deleted file mode 100644 index 84eea1f31..000000000 --- a/org.argeo.core/src/org/argeo/jcr/proxy/ResourceProxy.java +++ /dev/null @@ -1,16 +0,0 @@ -package org.argeo.jcr.proxy; - -import javax.jcr.Node; - -/** A proxy which nows how to resolve and synchronize relative URLs */ -public interface ResourceProxy { - /** - * Proxy the file referenced by this relative path in the underlying - * repository. A new session is created by each call, so the underlying - * session of the returned node must be closed by the caller. - * - * @return the proxied Node, null if the resource was not found - * (e.g. HTTP 404) - */ - public Node proxy(String relativePath); -} diff --git a/org.argeo.core/src/org/argeo/jcr/proxy/ResourceProxyServlet.java b/org.argeo.core/src/org/argeo/jcr/proxy/ResourceProxyServlet.java deleted file mode 100644 index d77bd49dc..000000000 --- a/org.argeo.core/src/org/argeo/jcr/proxy/ResourceProxyServlet.java +++ /dev/null @@ -1,116 +0,0 @@ -package org.argeo.jcr.proxy; - -import java.io.IOException; -import java.io.InputStream; - -import javax.jcr.Node; -import javax.jcr.Property; -import javax.jcr.RepositoryException; -import javax.servlet.ServletException; -import javax.servlet.http.HttpServlet; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -import org.apache.commons.io.FilenameUtils; -import org.apache.commons.io.IOUtils; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; -import org.argeo.jcr.JcrException; -import org.argeo.jcr.Bin; -import org.argeo.jcr.JcrUtils; - -/** Wraps a proxy via HTTP */ -public class ResourceProxyServlet extends HttpServlet { - private static final long serialVersionUID = -8886549549223155801L; - - private final static Log log = LogFactory - .getLog(ResourceProxyServlet.class); - - private ResourceProxy proxy; - - private String contentTypeCharset = "UTF-8"; - - @Override - protected void doGet(HttpServletRequest request, - HttpServletResponse response) throws ServletException, IOException { - String path = request.getPathInfo(); - - if (log.isTraceEnabled()) { - log.trace("path=" + path); - log.trace("UserPrincipal = " + request.getUserPrincipal().getName()); - log.trace("SessionID = " + request.getSession(false).getId()); - log.trace("ContextPath = " + request.getContextPath()); - log.trace("ServletPath = " + request.getServletPath()); - log.trace("PathInfo = " + request.getPathInfo()); - log.trace("Method = " + request.getMethod()); - log.trace("User-Agent = " + request.getHeader("User-Agent")); - } - - Node node = null; - try { - node = proxy.proxy(path); - if (node == null) - response.sendError(404); - else - processResponse(node, response); - } finally { - if (node != null) - try { - JcrUtils.logoutQuietly(node.getSession()); - } catch (RepositoryException e) { - // silent - } - } - - } - - /** Retrieve the content of the node. */ - protected void processResponse(Node node, HttpServletResponse response) { -// Binary binary = null; -// InputStream in = null; - try(Bin binary = new Bin( node.getNode(Property.JCR_CONTENT) - .getProperty(Property.JCR_DATA));InputStream in = binary.getStream()) { - String fileName = node.getName(); - String ext = FilenameUtils.getExtension(fileName); - - // TODO use a more generic / standard approach - // see http://svn.apache.org/viewvc/tomcat/trunk/conf/web.xml - String contentType; - if ("xml".equals(ext)) - contentType = "text/xml;charset=" + contentTypeCharset; - else if ("jar".equals(ext)) - contentType = "application/java-archive"; - else if ("zip".equals(ext)) - contentType = "application/zip"; - else if ("gz".equals(ext)) - contentType = "application/x-gzip"; - else if ("bz2".equals(ext)) - contentType = "application/x-bzip2"; - else if ("tar".equals(ext)) - contentType = "application/x-tar"; - else if ("rpm".equals(ext)) - contentType = "application/x-redhat-package-manager"; - else - contentType = "application/octet-stream"; - contentType = contentType + ";name=\"" + fileName + "\""; - response.setHeader("Content-Disposition", "attachment; filename=\"" - + fileName + "\""); - response.setHeader("Expires", "0"); - response.setHeader("Cache-Control", "no-cache, must-revalidate"); - response.setHeader("Pragma", "no-cache"); - - response.setContentType(contentType); - - IOUtils.copy(in, response.getOutputStream()); - } catch (RepositoryException e) { - throw new JcrException("Cannot download " + node, e); - } catch (IOException e) { - throw new RuntimeException("Cannot download " + node, e); - } - } - - public void setProxy(ResourceProxy resourceProxy) { - this.proxy = resourceProxy; - } - -} diff --git a/org.argeo.core/src/org/argeo/jcr/proxy/package-info.java b/org.argeo.core/src/org/argeo/jcr/proxy/package-info.java deleted file mode 100644 index a578c456e..000000000 --- a/org.argeo.core/src/org/argeo/jcr/proxy/package-info.java +++ /dev/null @@ -1,2 +0,0 @@ -/** Components to build proxys based on JCR. */ -package org.argeo.jcr.proxy; \ No newline at end of file diff --git a/org.argeo.core/src/org/argeo/jcr/unit/AbstractJcrTestCase.java b/org.argeo.core/src/org/argeo/jcr/unit/AbstractJcrTestCase.java deleted file mode 100644 index dc2963a51..000000000 --- a/org.argeo.core/src/org/argeo/jcr/unit/AbstractJcrTestCase.java +++ /dev/null @@ -1,116 +0,0 @@ -package org.argeo.jcr.unit; - -import java.io.File; -import java.security.AccessController; -import java.security.PrivilegedAction; - -import javax.jcr.Repository; -import javax.jcr.RepositoryException; -import javax.jcr.Session; -import javax.jcr.SimpleCredentials; -import javax.security.auth.Subject; -import javax.security.auth.login.LoginContext; -import javax.security.auth.login.LoginException; - -import org.apache.commons.io.FileUtils; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; -import org.argeo.jcr.JcrException; - -import junit.framework.TestCase; - -/** Base for unit tests with a JCR repository. */ -public abstract class AbstractJcrTestCase extends TestCase { - private final static Log log = LogFactory.getLog(AbstractJcrTestCase.class); - - private Repository repository; - private Session session = null; - - public final static String LOGIN_CONTEXT_TEST_SYSTEM = "TEST_JACKRABBIT_ADMIN"; - - // protected abstract File getRepositoryFile() throws Exception; - - protected abstract Repository createRepository() throws Exception; - - protected abstract void clearRepository(Repository repository) throws Exception; - - @Override - protected void setUp() throws Exception { - File homeDir = getHomeDir(); - FileUtils.deleteDirectory(homeDir); - repository = createRepository(); - } - - @Override - protected void tearDown() throws Exception { - if (session != null) { - session.logout(); - if (log.isTraceEnabled()) - log.trace("Logout session"); - } - clearRepository(repository); - } - - protected Session session() { - if (session != null && session.isLive()) - return session; - Session session; - if (getLoginContext() != null) { - LoginContext lc; - try { - lc = new LoginContext(getLoginContext()); - lc.login(); - } catch (LoginException e) { - throw new IllegalStateException("JAAS login failed", e); - } - session = Subject.doAs(lc.getSubject(), new PrivilegedAction() { - - @Override - public Session run() { - return login(); - } - - }); - } else - session = login(); - this.session = session; - return this.session; - } - - protected String getLoginContext() { - return null; - } - - protected Session login() { - try { - if (log.isTraceEnabled()) - log.trace("Login session"); - Subject subject = Subject.getSubject(AccessController.getContext()); - if (subject != null) - return getRepository().login(); - else - return getRepository().login(new SimpleCredentials("demo", "demo".toCharArray())); - } catch (RepositoryException e) { - throw new JcrException("Cannot login to repository", e); - } - } - - protected Repository getRepository() { - return repository; - } - - /** - * enables children class to set an existing repository in case it is not - * deleted on startup, to test migration by instance - */ - public void setRepository(Repository repository) { - this.repository = repository; - } - - protected File getHomeDir() { - File homeDir = new File(System.getProperty("java.io.tmpdir"), - AbstractJcrTestCase.class.getSimpleName() + "-" + System.getProperty("user.name")); - return homeDir; - } - -} diff --git a/org.argeo.core/src/org/argeo/jcr/unit/package-info.java b/org.argeo.core/src/org/argeo/jcr/unit/package-info.java deleted file mode 100644 index c6e741524..000000000 --- a/org.argeo.core/src/org/argeo/jcr/unit/package-info.java +++ /dev/null @@ -1,2 +0,0 @@ -/** Helpers for unit tests with JCR repositories. */ -package org.argeo.jcr.unit; \ No newline at end of file diff --git a/org.argeo.jcr/src/org/argeo/jcr/Bin.java b/org.argeo.jcr/src/org/argeo/jcr/Bin.java deleted file mode 100644 index 0418810ed..000000000 --- a/org.argeo.jcr/src/org/argeo/jcr/Bin.java +++ /dev/null @@ -1,60 +0,0 @@ -package org.argeo.jcr; - -import java.io.IOException; -import java.io.InputStream; - -import javax.jcr.Binary; -import javax.jcr.Property; -import javax.jcr.RepositoryException; - -/** - * A {@link Binary} wrapper implementing {@link AutoCloseable} for ease of use - * in try/catch blocks. - */ -public class Bin implements Binary, AutoCloseable { - private final Binary wrappedBinary; - - public Bin(Property property) throws RepositoryException { - this(property.getBinary()); - } - - public Bin(Binary wrappedBinary) { - if (wrappedBinary == null) - throw new IllegalArgumentException("Wrapped binary cannot be null"); - this.wrappedBinary = wrappedBinary; - } - - // private static Binary getBinary(Property property) throws IOException { - // try { - // return property.getBinary(); - // } catch (RepositoryException e) { - // throw new IOException("Cannot get binary from property " + property, e); - // } - // } - - @Override - public void close() { - dispose(); - } - - @Override - public InputStream getStream() throws RepositoryException { - return wrappedBinary.getStream(); - } - - @Override - public int read(byte[] b, long position) throws IOException, RepositoryException { - return wrappedBinary.read(b, position); - } - - @Override - public long getSize() throws RepositoryException { - return wrappedBinary.getSize(); - } - - @Override - public void dispose() { - wrappedBinary.dispose(); - } - -} diff --git a/org.argeo.jcr/src/org/argeo/jcr/CollectionNodeIterator.java b/org.argeo.jcr/src/org/argeo/jcr/CollectionNodeIterator.java deleted file mode 100644 index b4124eea5..000000000 --- a/org.argeo.jcr/src/org/argeo/jcr/CollectionNodeIterator.java +++ /dev/null @@ -1,61 +0,0 @@ -package org.argeo.jcr; - -import java.util.Collection; -import java.util.Iterator; -import java.util.NoSuchElementException; - -import javax.jcr.Node; -import javax.jcr.NodeIterator; - -/** Wraps a collection of nodes in order to read it as a {@link NodeIterator} */ -public class CollectionNodeIterator implements NodeIterator { - private final Long collectionSize; - private final Iterator iterator; - private Integer position = 0; - - public CollectionNodeIterator(Collection nodes) { - super(); - this.collectionSize = (long) nodes.size(); - this.iterator = nodes.iterator(); - } - - public void skip(long skipNum) { - if (skipNum < 0) - throw new IllegalArgumentException( - "Skip count has to be positive: " + skipNum); - - for (long i = 0; i < skipNum; i++) { - if (!hasNext()) - throw new NoSuchElementException("Last element past (position=" - + getPosition() + ")"); - nextNode(); - } - } - - public long getSize() { - return collectionSize; - } - - public long getPosition() { - return position; - } - - public boolean hasNext() { - return iterator.hasNext(); - } - - public Object next() { - return nextNode(); - } - - public void remove() { - iterator.remove(); - } - - public Node nextNode() { - Node node = iterator.next(); - position++; - return node; - } - -} diff --git a/org.argeo.jcr/src/org/argeo/jcr/DefaultJcrListener.java b/org.argeo.jcr/src/org/argeo/jcr/DefaultJcrListener.java deleted file mode 100644 index fc6888851..000000000 --- a/org.argeo.jcr/src/org/argeo/jcr/DefaultJcrListener.java +++ /dev/null @@ -1,78 +0,0 @@ -package org.argeo.jcr; - -import javax.jcr.RepositoryException; -import javax.jcr.Session; -import javax.jcr.observation.Event; -import javax.jcr.observation.EventIterator; -import javax.jcr.observation.EventListener; -import javax.jcr.observation.ObservationManager; - -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; - -/** To be overridden */ -public class DefaultJcrListener implements EventListener { - private final static Log log = LogFactory.getLog(DefaultJcrListener.class); - private Session session; - private String path = "/"; - private Boolean deep = true; - - public void start() { - try { - addEventListener(session().getWorkspace().getObservationManager()); - if (log.isDebugEnabled()) - log.debug("Registered JCR event listener on " + path); - } catch (RepositoryException e) { - throw new JcrException("Cannot register event listener", e); - } - } - - public void stop() { - try { - session().getWorkspace().getObservationManager() - .removeEventListener(this); - if (log.isDebugEnabled()) - log.debug("Unregistered JCR event listener on " + path); - } catch (RepositoryException e) { - throw new JcrException("Cannot unregister event listener", e); - } - } - - /** Default is listen to all events */ - protected Integer getEvents() { - return Event.NODE_ADDED | Event.NODE_REMOVED | Event.PROPERTY_ADDED - | Event.PROPERTY_CHANGED | Event.PROPERTY_REMOVED; - } - - /** To be overidden */ - public void onEvent(EventIterator events) { - while (events.hasNext()) { - Event event = events.nextEvent(); - log.debug(event); - } - } - - /** To be overidden */ - protected void addEventListener(ObservationManager observationManager) - throws RepositoryException { - observationManager.addEventListener(this, getEvents(), path, deep, - null, null, false); - } - - private Session session() { - return session; - } - - public void setPath(String path) { - this.path = path; - } - - public void setDeep(Boolean deep) { - this.deep = deep; - } - - public void setSession(Session session) { - this.session = session; - } - -} diff --git a/org.argeo.jcr/src/org/argeo/jcr/Jcr.java b/org.argeo.jcr/src/org/argeo/jcr/Jcr.java deleted file mode 100644 index 72e325d35..000000000 --- a/org.argeo.jcr/src/org/argeo/jcr/Jcr.java +++ /dev/null @@ -1,975 +0,0 @@ -package org.argeo.jcr; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.math.BigDecimal; -import java.text.MessageFormat; -import java.time.Instant; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Calendar; -import java.util.Collections; -import java.util.Date; -import java.util.GregorianCalendar; -import java.util.Iterator; -import java.util.List; - -import javax.jcr.Binary; -import javax.jcr.ItemNotFoundException; -import javax.jcr.Node; -import javax.jcr.NodeIterator; -import javax.jcr.Property; -import javax.jcr.PropertyType; -import javax.jcr.Repository; -import javax.jcr.RepositoryException; -import javax.jcr.Session; -import javax.jcr.Value; -import javax.jcr.Workspace; -import javax.jcr.nodetype.NodeType; -import javax.jcr.query.Query; -import javax.jcr.query.QueryManager; -import javax.jcr.security.Privilege; -import javax.jcr.version.Version; -import javax.jcr.version.VersionHistory; -import javax.jcr.version.VersionIterator; -import javax.jcr.version.VersionManager; - -import org.apache.commons.io.IOUtils; - -/** - * Utility class whose purpose is to make using JCR less verbose by - * systematically using unchecked exceptions and returning null - * when something is not found. This is especially useful when writing user - * interfaces (such as with SWT) where listeners and callbacks expect unchecked - * exceptions. Loosely inspired by Java's Files singleton. - */ -public class Jcr { - /** - * The name of a node which will be serialized as XML text, as per section 7.3.1 - * of the JCR 2.0 specifications. - */ - public final static String JCR_XMLTEXT = "jcr:xmltext"; - /** - * The name of a property which will be serialized as XML text, as per section - * 7.3.1 of the JCR 2.0 specifications. - */ - public final static String JCR_XMLCHARACTERS = "jcr:xmlcharacters"; - /** - * jcr:name, when used in another context than - * {@link Property#JCR_NAME}, typically to name a node rather than a property. - */ - public final static String JCR_NAME = "jcr:name"; - /** - * jcr:path, when used in another context than - * {@link Property#JCR_PATH}, typically to name a node rather than a property. - */ - public final static String JCR_PATH = "jcr:path"; - /** - * jcr:primaryType with prefix instead of namespace (as in - * {@link Property#JCR_PRIMARY_TYPE}. - */ - public final static String JCR_PRIMARY_TYPE = "jcr:primaryType"; - /** - * jcr:mixinTypes with prefix instead of namespace (as in - * {@link Property#JCR_MIXIN_TYPES}. - */ - public final static String JCR_MIXIN_TYPES = "jcr:mixinTypes"; - /** - * jcr:uuid with prefix instead of namespace (as in - * {@link Property#JCR_UUID}. - */ - public final static String JCR_UUID = "jcr:uuid"; - /** - * jcr:created with prefix instead of namespace (as in - * {@link Property#JCR_CREATED}. - */ - public final static String JCR_CREATED = "jcr:created"; - /** - * jcr:createdBy with prefix instead of namespace (as in - * {@link Property#JCR_CREATED_BY}. - */ - public final static String JCR_CREATED_BY = "jcr:createdBy"; - /** - * jcr:lastModified with prefix instead of namespace (as in - * {@link Property#JCR_LAST_MODIFIED}. - */ - public final static String JCR_LAST_MODIFIED = "jcr:lastModified"; - /** - * jcr:lastModifiedBy with prefix instead of namespace (as in - * {@link Property#JCR_LAST_MODIFIED_BY}. - */ - public final static String JCR_LAST_MODIFIED_BY = "jcr:lastModifiedBy"; - - /** - * @see Node#isNodeType(String) - * @throws JcrException caused by {@link RepositoryException} - */ - public static boolean isNodeType(Node node, String nodeTypeName) { - try { - return node.isNodeType(nodeTypeName); - } catch (RepositoryException e) { - throw new JcrException("Cannot get whether " + node + " is of type " + nodeTypeName, e); - } - } - - /** - * @see Node#hasNodes() - * @throws JcrException caused by {@link RepositoryException} - */ - public static boolean hasNodes(Node node) { - try { - return node.hasNodes(); - } catch (RepositoryException e) { - throw new JcrException("Cannot get whether " + node + " has children.", e); - } - } - - /** - * @see Node#getParent() - * @throws JcrException caused by {@link RepositoryException} - */ - public static Node getParent(Node node) { - try { - return isRoot(node) ? null : node.getParent(); - } catch (RepositoryException e) { - throw new JcrException("Cannot get parent of " + node, e); - } - } - - /** - * @see Node#getParent() - * @throws JcrException caused by {@link RepositoryException} - */ - public static String getParentPath(Node node) { - return getPath(getParent(node)); - } - - /** - * Whether this node is the root node. - * - * @throws JcrException caused by {@link RepositoryException} - */ - public static boolean isRoot(Node node) { - try { - return node.getDepth() == 0; - } catch (RepositoryException e) { - throw new JcrException("Cannot get depth of " + node, e); - } - } - - /** - * @see Node#getPath() - * @throws JcrException caused by {@link RepositoryException} - */ - public static String getPath(Node node) { - try { - return node.getPath(); - } catch (RepositoryException e) { - throw new JcrException("Cannot get path of " + node, e); - } - } - - /** - * @see Node#getSession() - * @see Session#getWorkspace() - * @see Workspace#getName() - */ - public static String getWorkspaceName(Node node) { - return session(node).getWorkspace().getName(); - } - - /** - * @see Node#getIdentifier() - * @throws JcrException caused by {@link RepositoryException} - */ - public static String getIdentifier(Node node) { - try { - return node.getIdentifier(); - } catch (RepositoryException e) { - throw new JcrException("Cannot get identifier of " + node, e); - } - } - - /** - * @see Node#getName() - * @throws JcrException caused by {@link RepositoryException} - */ - public static String getName(Node node) { - try { - return node.getName(); - } catch (RepositoryException e) { - throw new JcrException("Cannot get name of " + node, e); - } - } - - /** - * Returns the node name with its current index (useful for re-ordering). - * - * @see Node#getName() - * @see Node#getIndex() - * @throws JcrException caused by {@link RepositoryException} - */ - public static String getIndexedName(Node node) { - try { - return node.getName() + "[" + node.getIndex() + "]"; - } catch (RepositoryException e) { - throw new JcrException("Cannot get name of " + node, e); - } - } - - /** - * @see Node#getProperty(String) - * @throws JcrException caused by {@link RepositoryException} - */ - public static Property getProperty(Node node, String property) { - try { - if (node.hasProperty(property)) - return node.getProperty(property); - else - return null; - } catch (RepositoryException e) { - throw new JcrException("Cannot get property " + property + " of " + node, e); - } - } - - /** - * @see Node#getIndex() - * @throws JcrException caused by {@link RepositoryException} - */ - public static int getIndex(Node node) { - try { - return node.getIndex(); - } catch (RepositoryException e) { - throw new JcrException("Cannot get index of " + node, e); - } - } - - /** - * If node has mixin {@link NodeType#MIX_TITLE}, return - * {@link Property#JCR_TITLE}, otherwise return {@link #getName(Node)}. - */ - public static String getTitle(Node node) { - if (Jcr.isNodeType(node, NodeType.MIX_TITLE)) - return get(node, Property.JCR_TITLE); - else - return Jcr.getName(node); - } - - /** Accesses a {@link NodeIterator} as an {@link Iterable}. */ - @SuppressWarnings("unchecked") - public static Iterable iterate(NodeIterator nodeIterator) { - return new Iterable() { - - @Override - public Iterator iterator() { - return nodeIterator; - } - }; - } - - /** - * @return the children as an {@link Iterable} for use in for-each llops. - * @see Node#getNodes() - * @throws JcrException caused by {@link RepositoryException} - */ - public static Iterable nodes(Node node) { - try { - return iterate(node.getNodes()); - } catch (RepositoryException e) { - throw new JcrException("Cannot get children of " + node, e); - } - } - - /** - * @return the children as a (possibly empty) {@link List}. - * @see Node#getNodes() - * @throws JcrException caused by {@link RepositoryException} - */ - public static List getNodes(Node node) { - List nodes = new ArrayList<>(); - try { - if (node.hasNodes()) { - NodeIterator nit = node.getNodes(); - while (nit.hasNext()) - nodes.add(nit.nextNode()); - return nodes; - } else - return nodes; - } catch (RepositoryException e) { - throw new JcrException("Cannot get children of " + node, e); - } - } - - /** - * @return the child or null if not found - * @see Node#getNode(String) - * @throws JcrException caused by {@link RepositoryException} - */ - public static Node getNode(Node node, String child) { - try { - if (node.hasNode(child)) - return node.getNode(child); - else - return null; - } catch (RepositoryException e) { - throw new JcrException("Cannot get child of " + node, e); - } - } - - /** - * @return the node at this path or null if not found - * @see Session#getNode(String) - * @throws JcrException caused by {@link RepositoryException} - */ - public static Node getNode(Session session, String path) { - try { - if (session.nodeExists(path)) - return session.getNode(path); - else - return null; - } catch (RepositoryException e) { - throw new JcrException("Cannot get node " + path, e); - } - } - - /** - * Add a node to this parent, setting its primary type and its mixins. - * - * @param parent the parent node - * @param name the name of the node, if null, the primary - * type will be used (typically for XML structures) - * @param primaryType the primary type, if null - * {@link NodeType#NT_UNSTRUCTURED} will be used. - * @param mixins the mixins - * @return the created node - * @see Node#addNode(String, String) - * @see Node#addMixin(String) - */ - public static Node addNode(Node parent, String name, String primaryType, String... mixins) { - if (name == null && primaryType == null) - throw new IllegalArgumentException("Both node name and primary type cannot be null"); - try { - Node newNode = parent.addNode(name == null ? primaryType : name, - primaryType == null ? NodeType.NT_UNSTRUCTURED : primaryType); - for (String mixin : mixins) { - newNode.addMixin(mixin); - } - return newNode; - } catch (RepositoryException e) { - throw new JcrException("Cannot add node " + name + " to " + parent, e); - } - } - - /** - * Add an {@link NodeType#NT_BASE} node to this parent. - * - * @param parent the parent node - * @param name the name of the node, cannot be null - * @return the created node - * - * @see Node#addNode(String) - */ - public static Node addNode(Node parent, String name) { - if (name == null) - throw new IllegalArgumentException("Node name cannot be null"); - try { - Node newNode = parent.addNode(name); - return newNode; - } catch (RepositoryException e) { - throw new JcrException("Cannot add node " + name + " to " + parent, e); - } - } - - /** - * Add mixins to a node. - * - * @param node the node - * @param mixins the mixins - * @return the created node - * @see Node#addMixin(String) - */ - public static void addMixin(Node node, String... mixins) { - try { - for (String mixin : mixins) { - node.addMixin(mixin); - } - } catch (RepositoryException e) { - throw new JcrException("Cannot add mixins " + Arrays.asList(mixins) + " to " + node, e); - } - } - - /** - * Removes this node. - * - * @see Node#remove() - */ - public static void remove(Node node) { - try { - node.remove(); - } catch (RepositoryException e) { - throw new JcrException("Cannot remove node " + node, e); - } - } - - /** - * @return the node with htis id or null if not found - * @see Session#getNodeByIdentifier(String) - * @throws JcrException caused by {@link RepositoryException} - */ - public static Node getNodeById(Session session, String id) { - try { - return session.getNodeByIdentifier(id); - } catch (ItemNotFoundException e) { - return null; - } catch (RepositoryException e) { - throw new JcrException("Cannot get node with id " + id, e); - } - } - - /** - * Set a property to the given value, or remove it if the value is - * null. - * - * @throws JcrException caused by {@link RepositoryException} - */ - public static void set(Node node, String property, Object value) { - try { - if (!node.hasProperty(property)) { - if (value != null) { - if (value instanceof List) {// multiple - List lst = (List) value; - String[] values = new String[lst.size()]; - for (int i = 0; i < lst.size(); i++) { - values[i] = lst.get(i).toString(); - } - node.setProperty(property, values); - } else { - node.setProperty(property, value.toString()); - } - } - return; - } - Property prop = node.getProperty(property); - if (value == null) { - prop.remove(); - return; - } - - // multiple - if (value instanceof List) { - List lst = (List) value; - String[] values = new String[lst.size()]; - // TODO better cast? - for (int i = 0; i < lst.size(); i++) { - values[i] = lst.get(i).toString(); - } - if (!prop.isMultiple()) - prop.remove(); - node.setProperty(property, values); - return; - } - - // single - if (prop.isMultiple()) { - prop.remove(); - node.setProperty(property, value.toString()); - return; - } - - if (value instanceof String) - prop.setValue((String) value); - else if (value instanceof Long) - prop.setValue((Long) value); - else if (value instanceof Integer) - prop.setValue(((Integer) value).longValue()); - else if (value instanceof Double) - prop.setValue((Double) value); - else if (value instanceof Float) - prop.setValue(((Float) value).doubleValue()); - else if (value instanceof Calendar) - prop.setValue((Calendar) value); - else if (value instanceof BigDecimal) - prop.setValue((BigDecimal) value); - else if (value instanceof Boolean) - prop.setValue((Boolean) value); - else if (value instanceof byte[]) - JcrUtils.setBinaryAsBytes(prop, (byte[]) value); - else if (value instanceof Instant) { - Instant instant = (Instant) value; - GregorianCalendar calendar = new GregorianCalendar(); - calendar.setTime(Date.from(instant)); - prop.setValue(calendar); - } else // try with toString() - prop.setValue(value.toString()); - } catch (RepositoryException e) { - throw new JcrException("Cannot set property " + property + " of " + node + " to " + value, e); - } - } - - /** - * Get property as {@link String}. - * - * @return the value of - * {@link Node#getProperty(String)}.{@link Property#getString()} or - * null if the property does not exist. - * @throws JcrException caused by {@link RepositoryException} - */ - public static String get(Node node, String property) { - return get(node, property, null); - } - - /** - * Get property as a {@link String}. If the property is multiple it returns the - * first value. - * - * @return the value of - * {@link Node#getProperty(String)}.{@link Property#getString()} or - * defaultValue if the property does not exist. - * @throws JcrException caused by {@link RepositoryException} - */ - public static String get(Node node, String property, String defaultValue) { - try { - if (node.hasProperty(property)) { - Property p = node.getProperty(property); - if (!p.isMultiple()) - return p.getString(); - else { - Value[] values = p.getValues(); - if (values.length == 0) - return defaultValue; - else - return values[0].getString(); - } - } else - return defaultValue; - } catch (RepositoryException e) { - throw new JcrException("Cannot retrieve property " + property + " from " + node, e); - } - } - - /** - * Get property as a {@link Value}. - * - * @return {@link Node#getProperty(String)} or null if the property - * does not exist. - * @throws JcrException caused by {@link RepositoryException} - */ - public static Value getValue(Node node, String property) { - try { - if (node.hasProperty(property)) - return node.getProperty(property).getValue(); - else - return null; - } catch (RepositoryException e) { - throw new JcrException("Cannot retrieve property " + property + " from " + node, e); - } - } - - /** - * Get property doing a best effort to cast it as the target object. - * - * @return the value of {@link Node#getProperty(String)} or - * defaultValue if the property does not exist. - * @throws IllegalArgumentException if the value could not be cast - * @throws JcrException in case of unexpected - * {@link RepositoryException} - */ - @SuppressWarnings("unchecked") - public static T getAs(Node node, String property, T defaultValue) { - try { - // TODO deal with multiple - if (node.hasProperty(property)) { - Property p = node.getProperty(property); - try { - if (p.isMultiple()) { - throw new UnsupportedOperationException("Multiple values properties are not supported"); - } - Value value = p.getValue(); - return (T) get(value); - } catch (ClassCastException e) { - throw new IllegalArgumentException( - "Cannot cast property of type " + PropertyType.nameFromValue(p.getType()), e); - } - } else { - return defaultValue; - } - } catch (RepositoryException e) { - throw new JcrException("Cannot retrieve property " + property + " from " + node, e); - } - } - - /** - * Get a multiple property as a list, doing a best effort to cast it as the - * target list. - * - * @return the value of {@link Node#getProperty(String)}. - * @throws IllegalArgumentException if the value could not be cast - * @throws JcrException in case of unexpected - * {@link RepositoryException} - */ - public static List getMultiple(Node node, String property) { - try { - if (node.hasProperty(property)) { - Property p = node.getProperty(property); - return getMultiple(p); - } else { - return null; - } - } catch (RepositoryException e) { - throw new JcrException("Cannot retrieve multiple values property " + property + " from " + node, e); - } - } - - /** - * Get a multiple property as a list, doing a best effort to cast it as the - * target list. - */ - @SuppressWarnings("unchecked") - public static List getMultiple(Property p) { - try { - List res = new ArrayList<>(); - if (!p.isMultiple()) { - res.add((T) get(p.getValue())); - return res; - } - Value[] values = p.getValues(); - for (Value value : values) { - res.add((T) get(value)); - } - return res; - } catch (ClassCastException | RepositoryException e) { - throw new IllegalArgumentException("Cannot get property " + p, e); - } - } - - /** Cast a {@link Value} to a standard Java object. */ - public static Object get(Value value) { - Binary binary = null; - try { - switch (value.getType()) { - case PropertyType.STRING: - return value.getString(); - case PropertyType.DOUBLE: - return (Double) value.getDouble(); - case PropertyType.LONG: - return (Long) value.getLong(); - case PropertyType.BOOLEAN: - return (Boolean) value.getBoolean(); - case PropertyType.DATE: - return value.getDate(); - case PropertyType.BINARY: - binary = value.getBinary(); - byte[] arr = null; - try (InputStream in = binary.getStream(); ByteArrayOutputStream out = new ByteArrayOutputStream();) { - IOUtils.copy(in, out); - arr = out.toByteArray(); - } catch (IOException e) { - throw new RuntimeException("Cannot read binary from " + value, e); - } - return arr; - default: - return value.getString(); - } - } catch (RepositoryException e) { - throw new JcrException("Cannot cast value from " + value, e); - } finally { - if (binary != null) - binary.dispose(); - } - } - - /** - * Retrieves the {@link Session} related to this node. - * - * @deprecated Use {@link #getSession(Node)} instead. - */ - @Deprecated - public static Session session(Node node) { - return getSession(node); - } - - /** Retrieves the {@link Session} related to this node. */ - public static Session getSession(Node node) { - try { - return node.getSession(); - } catch (RepositoryException e) { - throw new JcrException("Cannot retrieve session related to " + node, e); - } - } - - /** Retrieves the root node related to this session. */ - public static Node getRootNode(Session session) { - try { - return session.getRootNode(); - } catch (RepositoryException e) { - throw new JcrException("Cannot get root node for " + session, e); - } - } - - /** Whether this item exists. */ - public static boolean itemExists(Session session, String path) { - try { - return session.itemExists(path); - } catch (RepositoryException e) { - throw new JcrException("Cannot check whether " + path + " exists", e); - } - } - - /** - * Saves the {@link Session} related to this node. Note that all other unrelated - * modifications in this session will also be saved. - */ - public static void save(Node node) { - try { - Session session = node.getSession(); -// if (node.isNodeType(NodeType.MIX_LAST_MODIFIED)) { -// set(node, Property.JCR_LAST_MODIFIED, Instant.now()); -// set(node, Property.JCR_LAST_MODIFIED_BY, session.getUserID()); -// } - if (session.hasPendingChanges()) - session.save(); - } catch (RepositoryException e) { - throw new JcrException("Cannot save session related to " + node + " in workspace " - + session(node).getWorkspace().getName(), e); - } - } - - /** Login to a JCR repository. */ - public static Session login(Repository repository, String workspace) { - try { - return repository.login(workspace); - } catch (RepositoryException e) { - throw new IllegalArgumentException("Cannot login to repository", e); - } - } - - /** Safely and silently logs out a session. */ - public static void logout(Session session) { - try { - if (session != null) - if (session.isLive()) - session.logout(); - } catch (Exception e) { - // silent - } - } - - /** Safely and silently logs out the underlying session. */ - public static void logout(Node node) { - Jcr.logout(session(node)); - } - - /* - * SECURITY - */ - /** - * Add a single privilege to a node. - * - * @see Privilege - */ - public static void addPrivilege(Node node, String principal, String privilege) { - try { - Session session = node.getSession(); - JcrUtils.addPrivilege(session, node.getPath(), principal, privilege); - } catch (RepositoryException e) { - throw new JcrException("Cannot add privilege " + privilege + " to " + node, e); - } - } - - /* - * VERSIONING - */ - /** Get checked out status. */ - public static boolean isCheckedOut(Node node) { - try { - return node.isCheckedOut(); - } catch (RepositoryException e) { - throw new JcrException("Cannot retrieve checked out status of " + node, e); - } - } - - /** @see VersionManager#checkpoint(String) */ - public static void checkpoint(Node node) { - try { - versionManager(node).checkpoint(node.getPath()); - } catch (RepositoryException e) { - throw new JcrException("Cannot check in " + node, e); - } - } - - /** @see VersionManager#checkin(String) */ - public static void checkin(Node node) { - try { - versionManager(node).checkin(node.getPath()); - } catch (RepositoryException e) { - throw new JcrException("Cannot check in " + node, e); - } - } - - /** @see VersionManager#checkout(String) */ - public static void checkout(Node node) { - try { - versionManager(node).checkout(node.getPath()); - } catch (RepositoryException e) { - throw new JcrException("Cannot check out " + node, e); - } - } - - /** Get the {@link VersionManager} related to this node. */ - public static VersionManager versionManager(Node node) { - try { - return node.getSession().getWorkspace().getVersionManager(); - } catch (RepositoryException e) { - throw new JcrException("Cannot get version manager from " + node, e); - } - } - - /** Get the {@link VersionHistory} related to this node. */ - public static VersionHistory getVersionHistory(Node node) { - try { - return versionManager(node).getVersionHistory(node.getPath()); - } catch (RepositoryException e) { - throw new JcrException("Cannot get version history from " + node, e); - } - } - - /** - * The linear versions of this version history in reverse order and without the - * root version. - */ - public static List getLinearVersions(VersionHistory versionHistory) { - try { - List lst = new ArrayList<>(); - VersionIterator vit = versionHistory.getAllLinearVersions(); - while (vit.hasNext()) - lst.add(vit.nextVersion()); - lst.remove(0); - Collections.reverse(lst); - return lst; - } catch (RepositoryException e) { - throw new JcrException("Cannot get linear versions from " + versionHistory, e); - } - } - - /** The frozen node related to this {@link Version}. */ - public static Node getFrozenNode(Version version) { - try { - return version.getFrozenNode(); - } catch (RepositoryException e) { - throw new JcrException("Cannot get frozen node from " + version, e); - } - } - - /** Get the base {@link Version} related to this node. */ - public static Version getBaseVersion(Node node) { - try { - return versionManager(node).getBaseVersion(node.getPath()); - } catch (RepositoryException e) { - throw new JcrException("Cannot get base version from " + node, e); - } - } - - /* - * FILES - */ - /** - * Returns the size of this file. - * - * @see NodeType#NT_FILE - */ - public static long getFileSize(Node fileNode) { - try { - if (!fileNode.isNodeType(NodeType.NT_FILE)) - throw new IllegalArgumentException(fileNode + " must be a file."); - return getBinarySize(fileNode.getNode(Node.JCR_CONTENT).getProperty(Property.JCR_DATA).getBinary()); - } catch (RepositoryException e) { - throw new JcrException("Cannot get file size of " + fileNode, e); - } - } - - /** Returns the size of this {@link Binary}. */ - public static long getBinarySize(Binary binaryArg) { - try { - try (Bin binary = new Bin(binaryArg)) { - return binary.getSize(); - } - } catch (RepositoryException e) { - throw new JcrException("Cannot get file size of binary " + binaryArg, e); - } - } - - // QUERY - /** Creates a JCR-SQL2 query using {@link MessageFormat}. */ - public static Query createQuery(QueryManager qm, String sql, Object... args) { - // fix single quotes - sql = sql.replaceAll("'", "''"); - String query = MessageFormat.format(sql, args); - try { - return qm.createQuery(query, Query.JCR_SQL2); - } catch (RepositoryException e) { - throw new JcrException("Cannot create JCR-SQL2 query from " + query, e); - } - } - - /** Executes a JCR-SQL2 query using {@link MessageFormat}. */ - public static NodeIterator executeQuery(QueryManager qm, String sql, Object... args) { - Query query = createQuery(qm, sql, args); - try { - return query.execute().getNodes(); - } catch (RepositoryException e) { - throw new JcrException("Cannot execute query " + sql + " with arguments " + Arrays.asList(args), e); - } - } - - /** Executes a JCR-SQL2 query using {@link MessageFormat}. */ - public static NodeIterator executeQuery(Session session, String sql, Object... args) { - QueryManager queryManager; - try { - queryManager = session.getWorkspace().getQueryManager(); - } catch (RepositoryException e) { - throw new JcrException("Cannot get query manager from session " + session, e); - } - return executeQuery(queryManager, sql, args); - } - - /** - * Executes a JCR-SQL2 query using {@link MessageFormat}, which must return a - * single node at most. - * - * @return the node or null if not found. - */ - public static Node getNode(QueryManager qm, String sql, Object... args) { - NodeIterator nit = executeQuery(qm, sql, args); - if (nit.hasNext()) { - Node node = nit.nextNode(); - if (nit.hasNext()) - throw new IllegalStateException( - "Query " + sql + " with arguments " + Arrays.asList(args) + " returned more than one node."); - return node; - } else { - return null; - } - } - - /** - * Executes a JCR-SQL2 query using {@link MessageFormat}, which must return a - * single node at most. - * - * @return the node or null if not found. - */ - public static Node getNode(Session session, String sql, Object... args) { - QueryManager queryManager; - try { - queryManager = session.getWorkspace().getQueryManager(); - } catch (RepositoryException e) { - throw new JcrException("Cannot get query manager from session " + session, e); - } - return getNode(queryManager, sql, args); - } - - /** Singleton. */ - private Jcr() { - - } -} diff --git a/org.argeo.jcr/src/org/argeo/jcr/JcrAuthorizations.java b/org.argeo.jcr/src/org/argeo/jcr/JcrAuthorizations.java deleted file mode 100644 index 351929f8d..000000000 --- a/org.argeo.jcr/src/org/argeo/jcr/JcrAuthorizations.java +++ /dev/null @@ -1,207 +0,0 @@ -package org.argeo.jcr; - -import java.security.Principal; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import javax.jcr.Repository; -import javax.jcr.RepositoryException; -import javax.jcr.Session; -import javax.jcr.security.AccessControlManager; -import javax.jcr.security.Privilege; -import javax.naming.InvalidNameException; -import javax.naming.ldap.LdapName; - -/** Apply authorizations to a JCR repository. */ -public class JcrAuthorizations implements Runnable { - // private final static Log log = - // LogFactory.getLog(JcrAuthorizations.class); - - private Repository repository; - private String workspace = null; - - private String securityWorkspace = "security"; - - /** - * key := privilege1,privilege2/path/to/node
- * value := group1,group2,user1 - */ - private Map principalPrivileges = new HashMap(); - - public void run() { - String currentWorkspace = workspace; - Session session = null; - try { - if (workspace != null && workspace.equals("*")) { - session = repository.login(); - String[] workspaces = session.getWorkspace().getAccessibleWorkspaceNames(); - JcrUtils.logoutQuietly(session); - for (String wksp : workspaces) { - currentWorkspace = wksp; - if (currentWorkspace.equals(securityWorkspace)) - continue; - session = repository.login(currentWorkspace); - initAuthorizations(session); - JcrUtils.logoutQuietly(session); - } - } else { - session = repository.login(workspace); - initAuthorizations(session); - } - } catch (RepositoryException e) { - JcrUtils.discardQuietly(session); - throw new JcrException( - "Cannot set authorizations " + principalPrivileges + " on workspace " + currentWorkspace, e); - } finally { - JcrUtils.logoutQuietly(session); - } - } - - protected void processWorkspace(String workspace) { - Session session = null; - try { - session = repository.login(workspace); - initAuthorizations(session); - } catch (RepositoryException e) { - JcrUtils.discardQuietly(session); - throw new JcrException( - "Cannot set authorizations " + principalPrivileges + " on repository " + repository, e); - } finally { - JcrUtils.logoutQuietly(session); - } - } - - /** @deprecated call {@link #run()} instead. */ - @Deprecated - public void init() { - run(); - } - - protected void initAuthorizations(Session session) throws RepositoryException { - AccessControlManager acm = session.getAccessControlManager(); - - for (String privileges : principalPrivileges.keySet()) { - String path = null; - int slashIndex = privileges.indexOf('/'); - if (slashIndex == 0) { - throw new IllegalArgumentException("Privilege " + privileges + " badly formatted it starts with /"); - } else if (slashIndex > 0) { - path = privileges.substring(slashIndex); - privileges = privileges.substring(0, slashIndex); - } - - if (path == null) - path = "/"; - - List privs = new ArrayList(); - for (String priv : privileges.split(",")) { - privs.add(acm.privilegeFromName(priv)); - } - - String principalNames = principalPrivileges.get(privileges); - try { - new LdapName(principalNames); - // TODO differentiate groups and users ? - Principal principal = getOrCreatePrincipal(session, principalNames); - JcrUtils.addPrivileges(session, path, principal, privs); - } catch (InvalidNameException e) { - for (String principalName : principalNames.split(",")) { - Principal principal = getOrCreatePrincipal(session, principalName); - JcrUtils.addPrivileges(session, path, principal, privs); - // if (log.isDebugEnabled()) { - // StringBuffer privBuf = new StringBuffer(); - // for (Privilege priv : privs) - // privBuf.append(priv.getName()); - // log.debug("Added privileges " + privBuf + " to " - // + principal.getName() + " on " + path + " in '" - // + session.getWorkspace().getName() + "'"); - // } - } - } - } - - // if (log.isDebugEnabled()) - // log.debug("JCR authorizations applied on '" - // + session.getWorkspace().getName() + "'"); - } - - /** - * Returns a {@link SimplePrincipal}, does not check whether it exists since - * such capabilities is not provided by the standard JCR API. Can be - * overridden to provide smarter handling - */ - protected Principal getOrCreatePrincipal(Session session, String principalName) throws RepositoryException { - return new SimplePrincipal(principalName); - } - - // public static void addPrivileges(Session session, Principal principal, - // String path, List privs) throws RepositoryException { - // AccessControlManager acm = session.getAccessControlManager(); - // // search for an access control list - // AccessControlList acl = null; - // AccessControlPolicyIterator policyIterator = acm - // .getApplicablePolicies(path); - // if (policyIterator.hasNext()) { - // while (policyIterator.hasNext()) { - // AccessControlPolicy acp = policyIterator - // .nextAccessControlPolicy(); - // if (acp instanceof AccessControlList) - // acl = ((AccessControlList) acp); - // } - // } else { - // AccessControlPolicy[] existingPolicies = acm.getPolicies(path); - // for (AccessControlPolicy acp : existingPolicies) { - // if (acp instanceof AccessControlList) - // acl = ((AccessControlList) acp); - // } - // } - // - // if (acl != null) { - // acl.addAccessControlEntry(principal, - // privs.toArray(new Privilege[privs.size()])); - // acm.setPolicy(path, acl); - // session.save(); - // if (log.isDebugEnabled()) { - // StringBuffer buf = new StringBuffer(""); - // for (int i = 0; i < privs.size(); i++) { - // if (i != 0) - // buf.append(','); - // buf.append(privs.get(i).getName()); - // } - // log.debug("Added privilege(s) '" + buf + "' to '" - // + principal.getName() + "' on " + path - // + " from workspace '" - // + session.getWorkspace().getName() + "'"); - // } - // } else { - // throw new ArgeoJcrException("Don't know how to apply privileges " - // + privs + " to " + principal + " on " + path - // + " from workspace '" + session.getWorkspace().getName() - // + "'"); - // } - // } - - @Deprecated - public void setGroupPrivileges(Map groupPrivileges) { - this.principalPrivileges = groupPrivileges; - } - - public void setPrincipalPrivileges(Map principalPrivileges) { - this.principalPrivileges = principalPrivileges; - } - - public void setRepository(Repository repository) { - this.repository = repository; - } - - public void setWorkspace(String workspace) { - this.workspace = workspace; - } - - public void setSecurityWorkspace(String securityWorkspace) { - this.securityWorkspace = securityWorkspace; - } - -} diff --git a/org.argeo.jcr/src/org/argeo/jcr/JcrCallback.java b/org.argeo.jcr/src/org/argeo/jcr/JcrCallback.java deleted file mode 100644 index efbaabe82..000000000 --- a/org.argeo.jcr/src/org/argeo/jcr/JcrCallback.java +++ /dev/null @@ -1,15 +0,0 @@ -package org.argeo.jcr; - -import java.util.function.Function; - -import javax.jcr.Session; - -/** An arbitrary execution on a JCR session, optionally returning a result. */ -@FunctionalInterface -public interface JcrCallback extends Function { - /** @deprecated Use {@link #apply(Session)} instead. */ - @Deprecated - public default Object execute(Session session) { - return apply(session); - } -} diff --git a/org.argeo.jcr/src/org/argeo/jcr/JcrException.java b/org.argeo.jcr/src/org/argeo/jcr/JcrException.java deleted file mode 100644 index c77874376..000000000 --- a/org.argeo.jcr/src/org/argeo/jcr/JcrException.java +++ /dev/null @@ -1,22 +0,0 @@ -package org.argeo.jcr; - -import javax.jcr.RepositoryException; - -/** - * Wraps a {@link RepositoryException} in a {@link RuntimeException}. - */ -public class JcrException extends IllegalStateException { - private static final long serialVersionUID = -4530350094877964989L; - - public JcrException(String message, RepositoryException e) { - super(message, e); - } - - public JcrException(RepositoryException e) { - super(e); - } - - public RepositoryException getRepositoryCause() { - return (RepositoryException) getCause(); - } -} diff --git a/org.argeo.jcr/src/org/argeo/jcr/JcrMonitor.java b/org.argeo.jcr/src/org/argeo/jcr/JcrMonitor.java deleted file mode 100644 index 71cf961e0..000000000 --- a/org.argeo.jcr/src/org/argeo/jcr/JcrMonitor.java +++ /dev/null @@ -1,87 +0,0 @@ -package org.argeo.jcr; - - -/** - * Simple monitor abstraction. Inspired by Eclipse IProgressMOnitor, but without - * dependency to it. - */ -public interface JcrMonitor { - /** - * Constant indicating an unknown amount of work. - */ - public final static int UNKNOWN = -1; - - /** - * Notifies that the main task is beginning. This must only be called once - * on a given progress monitor instance. - * - * @param name - * the name (or description) of the main task - * @param totalWork - * the total number of work units into which the main task is - * been subdivided. If the value is UNKNOWN the - * implementation is free to indicate progress in a way which - * doesn't require the total number of work units in advance. - */ - public void beginTask(String name, int totalWork); - - /** - * Notifies that the work is done; that is, either the main task is - * completed or the user canceled it. This method may be called more than - * once (implementations should be prepared to handle this case). - */ - public void done(); - - /** - * Returns whether cancelation of current operation has been requested. - * Long-running operations should poll to see if cancelation has been - * requested. - * - * @return true if cancellation has been requested, and - * false otherwise - * @see #setCanceled(boolean) - */ - public boolean isCanceled(); - - /** - * Sets the cancel state to the given value. - * - * @param value - * true indicates that cancelation has been - * requested (but not necessarily acknowledged); - * false clears this flag - * @see #isCanceled() - */ - public void setCanceled(boolean value); - - /** - * Sets the task name to the given value. This method is used to restore the - * task label after a nested operation was executed. Normally there is no - * need for clients to call this method. - * - * @param name - * the name (or description) of the main task - * @see #beginTask(java.lang.String, int) - */ - public void setTaskName(String name); - - /** - * Notifies that a subtask of the main task is beginning. Subtasks are - * optional; the main task might not have subtasks. - * - * @param name - * the name (or description) of the subtask - */ - public void subTask(String name); - - /** - * Notifies that a given number of work unit of the main task has been - * completed. Note that this amount represents an installment, as opposed to - * a cumulative amount of work done to date. - * - * @param work - * a non-negative number of work units just completed - */ - public void worked(int work); - -} diff --git a/org.argeo.jcr/src/org/argeo/jcr/JcrRepositoryWrapper.java b/org.argeo.jcr/src/org/argeo/jcr/JcrRepositoryWrapper.java deleted file mode 100644 index 3228eee74..000000000 --- a/org.argeo.jcr/src/org/argeo/jcr/JcrRepositoryWrapper.java +++ /dev/null @@ -1,244 +0,0 @@ -package org.argeo.jcr; - -import java.io.InputStream; -import java.math.BigDecimal; -import java.util.Arrays; -import java.util.Calendar; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import javax.jcr.Binary; -import javax.jcr.Credentials; -import javax.jcr.LoginException; -import javax.jcr.NoSuchWorkspaceException; -import javax.jcr.PropertyType; -import javax.jcr.Repository; -import javax.jcr.RepositoryException; -import javax.jcr.Session; -import javax.jcr.Value; -import javax.jcr.ValueFormatException; - -/** - * Wrapper around a JCR repository which allows to simplify configuration and - * intercept some actions. It exposes itself as a {@link Repository}. - */ -public abstract class JcrRepositoryWrapper implements Repository { - // private final static Log log = LogFactory - // .getLog(JcrRepositoryWrapper.class); - - // wrapped repository - private Repository repository; - - private Map additionalDescriptors = new HashMap<>(); - - private Boolean autocreateWorkspaces = false; - - public JcrRepositoryWrapper(Repository repository) { - setRepository(repository); - } - - /** - * Empty constructor - */ - public JcrRepositoryWrapper() { - } - - // /** Initializes */ - // public void init() { - // } - // - // /** Shutdown the repository */ - // public void destroy() throws Exception { - // } - - protected void putDescriptor(String key, String value) { - if (Arrays.asList(getRepository().getDescriptorKeys()).contains(key)) - throw new IllegalArgumentException("Descriptor key " + key + " is already defined in wrapped repository"); - if (value == null) - additionalDescriptors.remove(key); - else - additionalDescriptors.put(key, value); - } - - /* - * DELEGATED JCR REPOSITORY METHODS - */ - - public String getDescriptor(String key) { - if (additionalDescriptors.containsKey(key)) - return additionalDescriptors.get(key); - return getRepository().getDescriptor(key); - } - - public String[] getDescriptorKeys() { - if (additionalDescriptors.size() == 0) - return getRepository().getDescriptorKeys(); - List keys = Arrays.asList(getRepository().getDescriptorKeys()); - keys.addAll(additionalDescriptors.keySet()); - return keys.toArray(new String[keys.size()]); - } - - /** Central login method */ - public Session login(Credentials credentials, String workspaceName) - throws LoginException, NoSuchWorkspaceException, RepositoryException { - Session session; - try { - session = getRepository(workspaceName).login(credentials, workspaceName); - } catch (NoSuchWorkspaceException e) { - if (autocreateWorkspaces && workspaceName != null) - session = createWorkspaceAndLogsIn(credentials, workspaceName); - else - throw e; - } - processNewSession(session, workspaceName); - return session; - } - - public Session login() throws LoginException, RepositoryException { - return login(null, null); - } - - public Session login(Credentials credentials) throws LoginException, RepositoryException { - return login(credentials, null); - } - - public Session login(String workspaceName) throws LoginException, NoSuchWorkspaceException, RepositoryException { - return login(null, workspaceName); - } - - /** Called after a session has been created, does nothing by default. */ - protected void processNewSession(Session session, String workspaceName) { - } - - /** - * Wraps access to the repository, making sure it is available. - * - * @deprecated Use {@link #getDefaultRepository()} instead. - */ - @Deprecated - protected synchronized Repository getRepository() { - return getDefaultRepository(); - } - - protected synchronized Repository getDefaultRepository() { - return repository; - } - - protected synchronized Repository getRepository(String workspaceName) { - return getDefaultRepository(); - } - - /** - * Logs in to the default workspace, creates the required workspace, logs out, - * logs in to the required workspace. - */ - protected Session createWorkspaceAndLogsIn(Credentials credentials, String workspaceName) - throws RepositoryException { - if (workspaceName == null) - throw new IllegalArgumentException("No workspace specified."); - Session session = getRepository(workspaceName).login(credentials); - session.getWorkspace().createWorkspace(workspaceName); - session.logout(); - return getRepository(workspaceName).login(credentials, workspaceName); - } - - public boolean isStandardDescriptor(String key) { - return getRepository().isStandardDescriptor(key); - } - - public boolean isSingleValueDescriptor(String key) { - if (additionalDescriptors.containsKey(key)) - return true; - return getRepository().isSingleValueDescriptor(key); - } - - public Value getDescriptorValue(String key) { - if (additionalDescriptors.containsKey(key)) - return new StrValue(additionalDescriptors.get(key)); - return getRepository().getDescriptorValue(key); - } - - public Value[] getDescriptorValues(String key) { - return getRepository().getDescriptorValues(key); - } - - public synchronized void setRepository(Repository repository) { - this.repository = repository; - } - - public void setAutocreateWorkspaces(Boolean autocreateWorkspaces) { - this.autocreateWorkspaces = autocreateWorkspaces; - } - - protected static class StrValue implements Value { - private final String str; - - public StrValue(String str) { - this.str = str; - } - - @Override - public String getString() throws ValueFormatException, IllegalStateException, RepositoryException { - return str; - } - - @Override - public InputStream getStream() throws RepositoryException { - throw new UnsupportedOperationException(); - } - - @Override - public Binary getBinary() throws RepositoryException { - throw new UnsupportedOperationException(); - } - - @Override - public long getLong() throws ValueFormatException, RepositoryException { - try { - return Long.parseLong(str); - } catch (NumberFormatException e) { - throw new ValueFormatException("Cannot convert", e); - } - } - - @Override - public double getDouble() throws ValueFormatException, RepositoryException { - try { - return Double.parseDouble(str); - } catch (NumberFormatException e) { - throw new ValueFormatException("Cannot convert", e); - } - } - - @Override - public BigDecimal getDecimal() throws ValueFormatException, RepositoryException { - try { - return new BigDecimal(str); - } catch (NumberFormatException e) { - throw new ValueFormatException("Cannot convert", e); - } - } - - @Override - public Calendar getDate() throws ValueFormatException, RepositoryException { - throw new UnsupportedOperationException(); - } - - @Override - public boolean getBoolean() throws ValueFormatException, RepositoryException { - try { - return Boolean.parseBoolean(str); - } catch (NumberFormatException e) { - throw new ValueFormatException("Cannot convert", e); - } - } - - @Override - public int getType() { - return PropertyType.STRING; - } - - } - -} diff --git a/org.argeo.jcr/src/org/argeo/jcr/JcrUrlStreamHandler.java b/org.argeo.jcr/src/org/argeo/jcr/JcrUrlStreamHandler.java deleted file mode 100644 index 82a65e7f1..000000000 --- a/org.argeo.jcr/src/org/argeo/jcr/JcrUrlStreamHandler.java +++ /dev/null @@ -1,70 +0,0 @@ -package org.argeo.jcr; - -import java.io.IOException; -import java.io.InputStream; -import java.net.URL; -import java.net.URLConnection; -import java.net.URLStreamHandler; - -import javax.jcr.Item; -import javax.jcr.Node; -import javax.jcr.Property; -import javax.jcr.PropertyType; -import javax.jcr.RepositoryException; -import javax.jcr.Session; -import javax.jcr.nodetype.NodeType; - -/** URL stream handler able to deal with nt:file node and properties. NOT FINISHED */ -public class JcrUrlStreamHandler extends URLStreamHandler { - private final Session session; - - public JcrUrlStreamHandler(Session session) { - this.session = session; - } - - @Override - protected URLConnection openConnection(final URL u) throws IOException { - // TODO Auto-generated method stub - return new URLConnection(u) { - - @Override - public void connect() throws IOException { - String itemPath = u.getPath(); - try { - if (!session.itemExists(itemPath)) - throw new IOException("No item under " + itemPath); - - Item item = session.getItem(u.getPath()); - if (item.isNode()) { - // this should be a nt:file node - Node node = (Node) item; - if (!node.getPrimaryNodeType().isNodeType( - NodeType.NT_FILE)) - throw new IOException("Node " + node + " is not a " - + NodeType.NT_FILE); - - } else { - Property property = (Property) item; - if(property.getType()==PropertyType.BINARY){ - //Binary binary = property.getBinary(); - - } - } - } catch (RepositoryException e) { - IOException ioe = new IOException( - "Unexpected JCR exception"); - ioe.initCause(e); - throw ioe; - } - } - - @Override - public InputStream getInputStream() throws IOException { - // TODO Auto-generated method stub - return super.getInputStream(); - } - - }; - } - -} diff --git a/org.argeo.jcr/src/org/argeo/jcr/JcrUtils.java b/org.argeo.jcr/src/org/argeo/jcr/JcrUtils.java deleted file mode 100644 index 3be8be184..000000000 --- a/org.argeo.jcr/src/org/argeo/jcr/JcrUtils.java +++ /dev/null @@ -1,1778 +0,0 @@ -package org.argeo.jcr; - -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.io.PipedInputStream; -import java.io.PipedOutputStream; -import java.net.MalformedURLException; -import java.net.URL; -import java.nio.file.Files; -import java.nio.file.Path; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; -import java.security.Principal; -import java.text.DateFormat; -import java.text.ParseException; -import java.time.Instant; -import java.util.ArrayList; -import java.util.Calendar; -import java.util.Collections; -import java.util.Date; -import java.util.GregorianCalendar; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.TreeMap; - -import javax.jcr.Binary; -import javax.jcr.Credentials; -import javax.jcr.ImportUUIDBehavior; -import javax.jcr.NamespaceRegistry; -import javax.jcr.NoSuchWorkspaceException; -import javax.jcr.Node; -import javax.jcr.NodeIterator; -import javax.jcr.Property; -import javax.jcr.PropertyIterator; -import javax.jcr.PropertyType; -import javax.jcr.Repository; -import javax.jcr.RepositoryException; -import javax.jcr.Session; -import javax.jcr.Value; -import javax.jcr.Workspace; -import javax.jcr.nodetype.NoSuchNodeTypeException; -import javax.jcr.nodetype.NodeType; -import javax.jcr.observation.EventListener; -import javax.jcr.query.Query; -import javax.jcr.query.QueryResult; -import javax.jcr.security.AccessControlEntry; -import javax.jcr.security.AccessControlList; -import javax.jcr.security.AccessControlManager; -import javax.jcr.security.AccessControlPolicy; -import javax.jcr.security.AccessControlPolicyIterator; -import javax.jcr.security.Privilege; - -import org.apache.commons.io.IOUtils; - -/** Utility methods to simplify common JCR operations. */ -public class JcrUtils { - -// final private static Log log = LogFactory.getLog(JcrUtils.class); - - /** - * Not complete yet. See - * http://www.day.com/specs/jcr/2.0/3_Repository_Model.html#3.2.2%20Local - * %20Names - */ - public final static char[] INVALID_NAME_CHARACTERS = { '/', ':', '[', ']', '|', '*', /* invalid for XML: */ '<', - '>', '&' }; - - /** Prevents instantiation */ - private JcrUtils() { - } - - /** - * Queries one single node. - * - * @return one single node or null if none was found - * @throws JcrException if more than one node was found - */ - public static Node querySingleNode(Query query) { - NodeIterator nodeIterator; - try { - QueryResult queryResult = query.execute(); - nodeIterator = queryResult.getNodes(); - } catch (RepositoryException e) { - throw new JcrException("Cannot execute query " + query, e); - } - Node node; - if (nodeIterator.hasNext()) - node = nodeIterator.nextNode(); - else - return null; - - if (nodeIterator.hasNext()) - throw new IllegalArgumentException("Query returned more than one node."); - return node; - } - - /** Retrieves the node name from the provided path */ - public static String nodeNameFromPath(String path) { - if (path.equals("/")) - return ""; - if (path.charAt(0) != '/') - throw new IllegalArgumentException("Path " + path + " must start with a '/'"); - String pathT = path; - if (pathT.charAt(pathT.length() - 1) == '/') - pathT = pathT.substring(0, pathT.length() - 2); - - int index = pathT.lastIndexOf('/'); - return pathT.substring(index + 1); - } - - /** Retrieves the parent path of the provided path */ - public static String parentPath(String path) { - if (path.equals("/")) - throw new IllegalArgumentException("Root path '/' has no parent path"); - if (path.charAt(0) != '/') - throw new IllegalArgumentException("Path " + path + " must start with a '/'"); - String pathT = path; - if (pathT.charAt(pathT.length() - 1) == '/') - pathT = pathT.substring(0, pathT.length() - 2); - - int index = pathT.lastIndexOf('/'); - return pathT.substring(0, index); - } - - /** The provided data as a path ('/' at the end, not the beginning) */ - public static String dateAsPath(Calendar cal) { - return dateAsPath(cal, false); - } - - /** - * Creates a deep path based on a URL: - * http://subdomain.example.com/to/content?args becomes - * com/example/subdomain/to/content - */ - public static String urlAsPath(String url) { - try { - URL u = new URL(url); - StringBuffer path = new StringBuffer(url.length()); - // invert host - path.append(hostAsPath(u.getHost())); - // we don't put port since it may not always be there and may change - path.append(u.getPath()); - return path.toString(); - } catch (MalformedURLException e) { - throw new IllegalArgumentException("Cannot generate URL path for " + url, e); - } - } - - /** Set the {@link NodeType#NT_ADDRESS} properties based on this URL. */ - public static void urlToAddressProperties(Node node, String url) { - try { - URL u = new URL(url); - node.setProperty(Property.JCR_PROTOCOL, u.getProtocol()); - node.setProperty(Property.JCR_HOST, u.getHost()); - node.setProperty(Property.JCR_PORT, Integer.toString(u.getPort())); - node.setProperty(Property.JCR_PATH, normalizePath(u.getPath())); - } catch (RepositoryException e) { - throw new JcrException("Cannot set URL " + url + " as nt:address properties", e); - } catch (MalformedURLException e) { - throw new IllegalArgumentException("Cannot set URL " + url + " as nt:address properties", e); - } - } - - /** Build URL based on the {@link NodeType#NT_ADDRESS} properties. */ - public static String urlFromAddressProperties(Node node) { - try { - URL u = new URL(node.getProperty(Property.JCR_PROTOCOL).getString(), - node.getProperty(Property.JCR_HOST).getString(), - (int) node.getProperty(Property.JCR_PORT).getLong(), - node.getProperty(Property.JCR_PATH).getString()); - return u.toString(); - } catch (RepositoryException e) { - throw new JcrException("Cannot get URL from nt:address properties of " + node, e); - } catch (MalformedURLException e) { - throw new IllegalArgumentException("Cannot get URL from nt:address properties of " + node, e); - } - } - - /* - * PATH UTILITIES - */ - - /** - * Make sure that: starts with '/', do not end with '/', do not have '//' - */ - public static String normalizePath(String path) { - List tokens = tokenize(path); - StringBuffer buf = new StringBuffer(path.length()); - for (String token : tokens) { - buf.append('/'); - buf.append(token); - } - return buf.toString(); - } - - /** - * Creates a path from a FQDN, inverting the order of the component: - * www.argeo.org becomes org.argeo.www - */ - public static String hostAsPath(String host) { - StringBuffer path = new StringBuffer(host.length()); - String[] hostTokens = host.split("\\."); - for (int i = hostTokens.length - 1; i >= 0; i--) { - path.append(hostTokens[i]); - if (i != 0) - path.append('/'); - } - return path.toString(); - } - - /** - * Creates a path from a UUID (e.g. 6ebda899-217d-4bf1-abe4-2839085c8f3c becomes - * 6ebda899-217d/4bf1/abe4/2839085c8f3c/). '/' at the end, not the beginning - */ - public static String uuidAsPath(String uuid) { - StringBuffer path = new StringBuffer(uuid.length()); - String[] tokens = uuid.split("-"); - for (int i = 0; i < tokens.length; i++) { - path.append(tokens[i]); - if (i != 0) - path.append('/'); - } - return path.toString(); - } - - /** - * The provided data as a path ('/' at the end, not the beginning) - * - * @param cal the date - * @param addHour whether to add hour as well - */ - public static String dateAsPath(Calendar cal, Boolean addHour) { - StringBuffer buf = new StringBuffer(14); - buf.append('Y'); - buf.append(cal.get(Calendar.YEAR)); - buf.append('/'); - - int month = cal.get(Calendar.MONTH) + 1; - buf.append('M'); - if (month < 10) - buf.append(0); - buf.append(month); - buf.append('/'); - - int day = cal.get(Calendar.DAY_OF_MONTH); - buf.append('D'); - if (day < 10) - buf.append(0); - buf.append(day); - buf.append('/'); - - if (addHour) { - int hour = cal.get(Calendar.HOUR_OF_DAY); - buf.append('H'); - if (hour < 10) - buf.append(0); - buf.append(hour); - buf.append('/'); - } - return buf.toString(); - - } - - /** Converts in one call a string into a gregorian calendar. */ - public static Calendar parseCalendar(DateFormat dateFormat, String value) { - try { - Date date = dateFormat.parse(value); - Calendar calendar = new GregorianCalendar(); - calendar.setTime(date); - return calendar; - } catch (ParseException e) { - throw new IllegalArgumentException("Cannot parse " + value + " with date format " + dateFormat, e); - } - - } - - /** The last element of a path. */ - public static String lastPathElement(String path) { - if (path.charAt(path.length() - 1) == '/') - throw new IllegalArgumentException("Path " + path + " cannot end with '/'"); - int index = path.lastIndexOf('/'); - if (index < 0) - return path; - return path.substring(index + 1); - } - - /** - * Call {@link Node#getName()} without exceptions (useful in super - * constructors). - */ - public static String getNameQuietly(Node node) { - try { - return node.getName(); - } catch (RepositoryException e) { - throw new JcrException("Cannot get name from " + node, e); - } - } - - /** - * Call {@link Node#getProperty(String)} without exceptions (useful in super - * constructors). - */ - public static String getStringPropertyQuietly(Node node, String propertyName) { - try { - return node.getProperty(propertyName).getString(); - } catch (RepositoryException e) { - throw new JcrException("Cannot get name from " + node, e); - } - } - -// /** -// * Routine that get the child with this name, adding it if it does not already -// * exist -// */ -// public static Node getOrAdd(Node parent, String name, String primaryNodeType) throws RepositoryException { -// return parent.hasNode(name) ? parent.getNode(name) : parent.addNode(name, primaryNodeType); -// } - - /** - * Routine that get the child with this name, adding it if it does not already - * exist - */ - public static Node getOrAdd(Node parent, String name, String primaryNodeType, String... mixinNodeTypes) - throws RepositoryException { - Node node; - if (parent.hasNode(name)) { - node = parent.getNode(name); - if (primaryNodeType != null && !node.isNodeType(primaryNodeType)) - throw new IllegalArgumentException("Node " + node + " exists but is of primary node type " - + node.getPrimaryNodeType().getName() + ", not " + primaryNodeType); - for (String mixin : mixinNodeTypes) { - if (!node.isNodeType(mixin)) - node.addMixin(mixin); - } - return node; - } else { - node = primaryNodeType != null ? parent.addNode(name, primaryNodeType) : parent.addNode(name); - for (String mixin : mixinNodeTypes) { - node.addMixin(mixin); - } - return node; - } - } - - /** - * Routine that get the child with this name, adding it if it does not already - * exist - */ - public static Node getOrAdd(Node parent, String name) throws RepositoryException { - return parent.hasNode(name) ? parent.getNode(name) : parent.addNode(name); - } - - /** Convert a {@link NodeIterator} to a list of {@link Node} */ - public static List nodeIteratorToList(NodeIterator nodeIterator) { - List nodes = new ArrayList(); - while (nodeIterator.hasNext()) { - nodes.add(nodeIterator.nextNode()); - } - return nodes; - } - - /* - * PROPERTIES - */ - - /** - * Concisely get the string value of a property or null if this node doesn't - * have this property - */ - public static String get(Node node, String propertyName) { - try { - if (!node.hasProperty(propertyName)) - return null; - return node.getProperty(propertyName).getString(); - } catch (RepositoryException e) { - throw new JcrException("Cannot get property " + propertyName + " of " + node, e); - } - } - - /** Concisely get the path of the given node. */ - public static String getPath(Node node) { - try { - return node.getPath(); - } catch (RepositoryException e) { - throw new JcrException("Cannot get path of " + node, e); - } - } - - /** Concisely get the boolean value of a property */ - public static Boolean check(Node node, String propertyName) { - try { - return node.getProperty(propertyName).getBoolean(); - } catch (RepositoryException e) { - throw new JcrException("Cannot get property " + propertyName + " of " + node, e); - } - } - - /** Concisely get the bytes array value of a property */ - public static byte[] getBytes(Node node, String propertyName) { - try { - return getBinaryAsBytes(node.getProperty(propertyName)); - } catch (RepositoryException e) { - throw new JcrException("Cannot get property " + propertyName + " of " + node, e); - } - } - - /* - * MKDIRS - */ - - /** - * Create sub nodes relative to a parent node - */ - public static Node mkdirs(Node parentNode, String relativePath) { - return mkdirs(parentNode, relativePath, null, null); - } - - /** - * Create sub nodes relative to a parent node - * - * @param nodeType the type of the leaf node - */ - public static Node mkdirs(Node parentNode, String relativePath, String nodeType) { - return mkdirs(parentNode, relativePath, nodeType, null); - } - - /** - * Create sub nodes relative to a parent node - * - * @param nodeType the type of the leaf node - */ - public static Node mkdirs(Node parentNode, String relativePath, String nodeType, String intermediaryNodeType) { - List tokens = tokenize(relativePath); - Node currParent = parentNode; - try { - for (int i = 0; i < tokens.size(); i++) { - String name = tokens.get(i); - if (currParent.hasNode(name)) { - currParent = currParent.getNode(name); - } else { - if (i != (tokens.size() - 1)) {// intermediary - currParent = currParent.addNode(name, intermediaryNodeType); - } else {// leaf - currParent = currParent.addNode(name, nodeType); - } - } - } - return currParent; - } catch (RepositoryException e) { - throw new JcrException("Cannot mkdirs relative path " + relativePath + " from " + parentNode, e); - } - } - - /** - * Synchronized and save is performed, to avoid race conditions in initializers - * leading to duplicate nodes. - */ - public synchronized static Node mkdirsSafe(Session session, String path, String type) { - try { - if (session.hasPendingChanges()) - throw new IllegalStateException("Session has pending changes, save them first."); - Node node = mkdirs(session, path, type); - session.save(); - return node; - } catch (RepositoryException e) { - discardQuietly(session); - throw new JcrException("Cannot safely make directories", e); - } - } - - public synchronized static Node mkdirsSafe(Session session, String path) { - return mkdirsSafe(session, path, null); - } - - /** Creates the nodes making path, if they don't exist. */ - public static Node mkdirs(Session session, String path) { - return mkdirs(session, path, null, null, false); - } - - /** - * @param type the type of the leaf node - */ - public static Node mkdirs(Session session, String path, String type) { - return mkdirs(session, path, type, null, false); - } - - /** - * Creates the nodes making path, if they don't exist. This is up to the caller - * to save the session. Use with caution since it can create duplicate nodes if - * used concurrently. Requires read access to the root node of the workspace. - */ - public static Node mkdirs(Session session, String path, String type, String intermediaryNodeType, - Boolean versioning) { - try { - if (path.equals("/")) - return session.getRootNode(); - - if (session.itemExists(path)) { - Node node = session.getNode(path); - // check type - if (type != null && !node.isNodeType(type) && !node.getPath().equals("/")) - throw new IllegalArgumentException("Node " + node + " exists but is of type " - + node.getPrimaryNodeType().getName() + " not of type " + type); - // TODO: check versioning - return node; - } - - // StringBuffer current = new StringBuffer("/"); - // Node currentNode = session.getRootNode(); - - Node currentNode = findClosestExistingParent(session, path); - String closestExistingParentPath = currentNode.getPath(); - StringBuffer current = new StringBuffer(closestExistingParentPath); - if (!closestExistingParentPath.endsWith("/")) - current.append('/'); - Iterator it = tokenize(path.substring(closestExistingParentPath.length())).iterator(); - while (it.hasNext()) { - String part = it.next(); - current.append(part).append('/'); - if (!session.itemExists(current.toString())) { - if (!it.hasNext() && type != null) - currentNode = currentNode.addNode(part, type); - else if (it.hasNext() && intermediaryNodeType != null) - currentNode = currentNode.addNode(part, intermediaryNodeType); - else - currentNode = currentNode.addNode(part); - if (versioning) - currentNode.addMixin(NodeType.MIX_VERSIONABLE); -// if (log.isTraceEnabled()) -// log.debug("Added folder " + part + " as " + current); - } else { - currentNode = (Node) session.getItem(current.toString()); - } - } - return currentNode; - } catch (RepositoryException e) { - discardQuietly(session); - throw new JcrException("Cannot mkdirs " + path, e); - } finally { - } - } - - private static Node findClosestExistingParent(Session session, String path) throws RepositoryException { - int idx = path.lastIndexOf('/'); - if (idx == 0) - return session.getRootNode(); - String parentPath = path.substring(0, idx); - if (session.itemExists(parentPath)) - return session.getNode(parentPath); - else - return findClosestExistingParent(session, parentPath); - } - - /** Convert a path to the list of its tokens */ - public static List tokenize(String path) { - List tokens = new ArrayList(); - boolean optimized = false; - if (!optimized) { - String[] rawTokens = path.split("/"); - for (String token : rawTokens) { - if (!token.equals("")) - tokens.add(token); - } - } else { - StringBuffer curr = new StringBuffer(); - char[] arr = path.toCharArray(); - chars: for (int i = 0; i < arr.length; i++) { - char c = arr[i]; - if (c == '/') { - if (i == 0 || (i == arr.length - 1)) - continue chars; - if (curr.length() > 0) { - tokens.add(curr.toString()); - curr = new StringBuffer(); - } - } else - curr.append(c); - } - if (curr.length() > 0) { - tokens.add(curr.toString()); - curr = new StringBuffer(); - } - } - return Collections.unmodifiableList(tokens); - } - - // /** - // * use {@link #mkdirs(Session, String, String, String, Boolean)} instead. - // * - // * @deprecated - // */ - // @Deprecated - // public static Node mkdirs(Session session, String path, String type, - // Boolean versioning) { - // return mkdirs(session, path, type, type, false); - // } - - /** - * Safe and repository implementation independent registration of a namespace. - */ - public static void registerNamespaceSafely(Session session, String prefix, String uri) { - try { - registerNamespaceSafely(session.getWorkspace().getNamespaceRegistry(), prefix, uri); - } catch (RepositoryException e) { - throw new JcrException("Cannot find namespace registry", e); - } - } - - /** - * Safe and repository implementation independent registration of a namespace. - */ - public static void registerNamespaceSafely(NamespaceRegistry nr, String prefix, String uri) { - try { - String[] prefixes = nr.getPrefixes(); - for (String pref : prefixes) - if (pref.equals(prefix)) { - String registeredUri = nr.getURI(pref); - if (!registeredUri.equals(uri)) - throw new IllegalArgumentException("Prefix " + pref + " already registered for URI " - + registeredUri + " which is different from provided URI " + uri); - else - return;// skip - } - nr.registerNamespace(prefix, uri); - } catch (RepositoryException e) { - throw new JcrException("Cannot register namespace " + uri + " under prefix " + prefix, e); - } - } - -// /** Recursively outputs the contents of the given node. */ -// public static void debug(Node node) { -// debug(node, log); -// } -// -// /** Recursively outputs the contents of the given node. */ -// public static void debug(Node node, Log log) { -// try { -// // First output the node path -// log.debug(node.getPath()); -// // Skip the virtual (and large!) jcr:system subtree -// if (node.getName().equals("jcr:system")) { -// return; -// } -// -// // Then the children nodes (recursive) -// NodeIterator it = node.getNodes(); -// while (it.hasNext()) { -// Node childNode = it.nextNode(); -// debug(childNode, log); -// } -// -// // Then output the properties -// PropertyIterator properties = node.getProperties(); -// // log.debug("Property are : "); -// -// properties: while (properties.hasNext()) { -// Property property = properties.nextProperty(); -// if (property.getType() == PropertyType.BINARY) -// continue properties;// skip -// if (property.getDefinition().isMultiple()) { -// // A multi-valued property, print all values -// Value[] values = property.getValues(); -// for (int i = 0; i < values.length; i++) { -// log.debug(property.getPath() + "=" + values[i].getString()); -// } -// } else { -// // A single-valued property -// log.debug(property.getPath() + "=" + property.getString()); -// } -// } -// } catch (Exception e) { -// log.error("Could not debug " + node, e); -// } -// -// } - -// /** Logs the effective access control policies */ -// public static void logEffectiveAccessPolicies(Node node) { -// try { -// logEffectiveAccessPolicies(node.getSession(), node.getPath()); -// } catch (RepositoryException e) { -// log.error("Cannot log effective access policies of " + node, e); -// } -// } -// -// /** Logs the effective access control policies */ -// public static void logEffectiveAccessPolicies(Session session, String path) { -// if (!log.isDebugEnabled()) -// return; -// -// try { -// AccessControlPolicy[] effectivePolicies = session.getAccessControlManager().getEffectivePolicies(path); -// if (effectivePolicies.length > 0) { -// for (AccessControlPolicy policy : effectivePolicies) { -// if (policy instanceof AccessControlList) { -// AccessControlList acl = (AccessControlList) policy; -// log.debug("Access control list for " + path + "\n" + accessControlListSummary(acl)); -// } -// } -// } else { -// log.debug("No effective access control policy for " + path); -// } -// } catch (RepositoryException e) { -// log.error("Cannot log effective access policies of " + path, e); -// } -// } - - /** Returns a human-readable summary of this access control list. */ - public static String accessControlListSummary(AccessControlList acl) { - StringBuffer buf = new StringBuffer(""); - try { - for (AccessControlEntry ace : acl.getAccessControlEntries()) { - buf.append('\t').append(ace.getPrincipal().getName()).append('\n'); - for (Privilege priv : ace.getPrivileges()) - buf.append("\t\t").append(priv.getName()).append('\n'); - } - return buf.toString(); - } catch (RepositoryException e) { - throw new JcrException("Cannot write summary of " + acl, e); - } - } - - /** Copy the whole workspace via a system view XML. */ - public static void copyWorkspaceXml(Session fromSession, Session toSession) { - Workspace fromWorkspace = fromSession.getWorkspace(); - Workspace toWorkspace = toSession.getWorkspace(); - String errorMsg = "Cannot copy workspace " + fromWorkspace + " to " + toWorkspace + " via XML."; - - try (PipedInputStream in = new PipedInputStream(1024 * 1024);) { - new Thread(() -> { - try (PipedOutputStream out = new PipedOutputStream(in)) { - fromSession.exportSystemView("/", out, false, false); - out.flush(); - } catch (IOException e) { - throw new RuntimeException(errorMsg, e); - } catch (RepositoryException e) { - throw new JcrException(errorMsg, e); - } - }, "Copy workspace" + fromWorkspace + " to " + toWorkspace).start(); - - toSession.importXML("/", in, ImportUUIDBehavior.IMPORT_UUID_COLLISION_REPLACE_EXISTING); - toSession.save(); - } catch (IOException e) { - throw new RuntimeException(errorMsg, e); - } catch (RepositoryException e) { - throw new JcrException(errorMsg, e); - } - } - - /** - * Copies recursively the content of a node to another one. Do NOT copy the - * property values of {@link NodeType#MIX_CREATED} and - * {@link NodeType#MIX_LAST_MODIFIED}, but update the - * {@link Property#JCR_LAST_MODIFIED} and {@link Property#JCR_LAST_MODIFIED_BY} - * properties if the target node has the {@link NodeType#MIX_LAST_MODIFIED} - * mixin. - */ - public static void copy(Node fromNode, Node toNode) { - try { - if (toNode.getDefinition().isProtected()) - return; - - // add mixins - for (NodeType mixinType : fromNode.getMixinNodeTypes()) { - try { - toNode.addMixin(mixinType.getName()); - } catch (NoSuchNodeTypeException e) { - // ignore unknown mixins - // TODO log it - } - } - - // process properties - PropertyIterator pit = fromNode.getProperties(); - properties: while (pit.hasNext()) { - Property fromProperty = pit.nextProperty(); - String propertyName = fromProperty.getName(); - if (toNode.hasProperty(propertyName) && toNode.getProperty(propertyName).getDefinition().isProtected()) - continue properties; - - if (fromProperty.getDefinition().isProtected()) - continue properties; - - if (propertyName.equals("jcr:created") || propertyName.equals("jcr:createdBy") - || propertyName.equals("jcr:lastModified") || propertyName.equals("jcr:lastModifiedBy")) - continue properties; - - if (fromProperty.isMultiple()) { - toNode.setProperty(propertyName, fromProperty.getValues()); - } else { - toNode.setProperty(propertyName, fromProperty.getValue()); - } - } - - // update jcr:lastModified and jcr:lastModifiedBy in toNode in case - // they existed, before adding the mixins - updateLastModified(toNode, true); - - // process children nodes - NodeIterator nit = fromNode.getNodes(); - while (nit.hasNext()) { - Node fromChild = nit.nextNode(); - Integer index = fromChild.getIndex(); - String nodeRelPath = fromChild.getName() + "[" + index + "]"; - Node toChild; - if (toNode.hasNode(nodeRelPath)) - toChild = toNode.getNode(nodeRelPath); - else { - try { - toChild = toNode.addNode(fromChild.getName(), fromChild.getPrimaryNodeType().getName()); - } catch (NoSuchNodeTypeException e) { - // ignore unknown primary types - // TODO log it - return; - } - } - copy(fromChild, toChild); - } - } catch (RepositoryException e) { - throw new JcrException("Cannot copy " + fromNode + " to " + toNode, e); - } - } - - /** - * Check whether all first-level properties (except jcr:* properties) are equal. - * Skip jcr:* properties - */ - public static Boolean allPropertiesEquals(Node reference, Node observed, Boolean onlyCommonProperties) { - try { - PropertyIterator pit = reference.getProperties(); - props: while (pit.hasNext()) { - Property propReference = pit.nextProperty(); - String propName = propReference.getName(); - if (propName.startsWith("jcr:")) - continue props; - - if (!observed.hasProperty(propName)) - if (onlyCommonProperties) - continue props; - else - return false; - // TODO: deal with multiple property values? - if (!observed.getProperty(propName).getValue().equals(propReference.getValue())) - return false; - } - return true; - } catch (RepositoryException e) { - throw new JcrException("Cannot check all properties equals of " + reference + " and " + observed, e); - } - } - - public static Map diffProperties(Node reference, Node observed) { - Map diffs = new TreeMap(); - diffPropertiesLevel(diffs, null, reference, observed); - return diffs; - } - - /** - * Compare the properties of two nodes. Recursivity to child nodes is not yet - * supported. Skip jcr:* properties. - */ - static void diffPropertiesLevel(Map diffs, String baseRelPath, Node reference, - Node observed) { - try { - // check removed and modified - PropertyIterator pit = reference.getProperties(); - props: while (pit.hasNext()) { - Property p = pit.nextProperty(); - String name = p.getName(); - if (name.startsWith("jcr:")) - continue props; - - if (!observed.hasProperty(name)) { - String relPath = propertyRelPath(baseRelPath, name); - PropertyDiff pDiff = new PropertyDiff(PropertyDiff.REMOVED, relPath, p.getValue(), null); - diffs.put(relPath, pDiff); - } else { - if (p.isMultiple()) { - // FIXME implement multiple - } else { - Value referenceValue = p.getValue(); - Value newValue = observed.getProperty(name).getValue(); - if (!referenceValue.equals(newValue)) { - String relPath = propertyRelPath(baseRelPath, name); - PropertyDiff pDiff = new PropertyDiff(PropertyDiff.MODIFIED, relPath, referenceValue, - newValue); - diffs.put(relPath, pDiff); - } - } - } - } - // check added - pit = observed.getProperties(); - props: while (pit.hasNext()) { - Property p = pit.nextProperty(); - String name = p.getName(); - if (name.startsWith("jcr:")) - continue props; - if (!reference.hasProperty(name)) { - if (p.isMultiple()) { - // FIXME implement multiple - } else { - String relPath = propertyRelPath(baseRelPath, name); - PropertyDiff pDiff = new PropertyDiff(PropertyDiff.ADDED, relPath, null, p.getValue()); - diffs.put(relPath, pDiff); - } - } - } - } catch (RepositoryException e) { - throw new JcrException("Cannot diff " + reference + " and " + observed, e); - } - } - - /** - * Compare only a restricted list of properties of two nodes. No recursivity. - * - */ - public static Map diffProperties(Node reference, Node observed, List properties) { - Map diffs = new TreeMap(); - try { - Iterator pit = properties.iterator(); - - props: while (pit.hasNext()) { - String name = pit.next(); - if (!reference.hasProperty(name)) { - if (!observed.hasProperty(name)) - continue props; - Value val = observed.getProperty(name).getValue(); - try { - // empty String but not null - if ("".equals(val.getString())) - continue props; - } catch (Exception e) { - // not parseable as String, silent - } - PropertyDiff pDiff = new PropertyDiff(PropertyDiff.ADDED, name, null, val); - diffs.put(name, pDiff); - } else if (!observed.hasProperty(name)) { - PropertyDiff pDiff = new PropertyDiff(PropertyDiff.REMOVED, name, - reference.getProperty(name).getValue(), null); - diffs.put(name, pDiff); - } else { - Value referenceValue = reference.getProperty(name).getValue(); - Value newValue = observed.getProperty(name).getValue(); - if (!referenceValue.equals(newValue)) { - PropertyDiff pDiff = new PropertyDiff(PropertyDiff.MODIFIED, name, referenceValue, newValue); - diffs.put(name, pDiff); - } - } - } - } catch (RepositoryException e) { - throw new JcrException("Cannot diff " + reference + " and " + observed, e); - } - return diffs; - } - - /** Builds a property relPath to be used in the diff. */ - private static String propertyRelPath(String baseRelPath, String propertyName) { - if (baseRelPath == null) - return propertyName; - else - return baseRelPath + '/' + propertyName; - } - - /** - * Normalizes a name so that it can be stored in contexts not supporting names - * with ':' (typically databases). Replaces ':' by '_'. - */ - public static String normalize(String name) { - return name.replace(':', '_'); - } - - /** - * Replaces characters which are invalid in a JCR name by '_'. Currently not - * exhaustive. - * - * @see JcrUtils#INVALID_NAME_CHARACTERS - */ - public static String replaceInvalidChars(String name) { - return replaceInvalidChars(name, '_'); - } - - /** - * Replaces characters which are invalid in a JCR name. Currently not - * exhaustive. - * - * @see JcrUtils#INVALID_NAME_CHARACTERS - */ - public static String replaceInvalidChars(String name, char replacement) { - boolean modified = false; - char[] arr = name.toCharArray(); - for (int i = 0; i < arr.length; i++) { - char c = arr[i]; - invalid: for (char invalid : INVALID_NAME_CHARACTERS) { - if (c == invalid) { - arr[i] = replacement; - modified = true; - break invalid; - } - } - } - if (modified) - return new String(arr); - else - // do not create new object if unnecessary - return name; - } - - // /** - // * Removes forbidden characters from a path, replacing them with '_' - // * - // * @deprecated use {@link #replaceInvalidChars(String)} instead - // */ - // public static String removeForbiddenCharacters(String str) { - // return str.replace('[', '_').replace(']', '_').replace('/', '_').replace('*', - // '_'); - // - // } - - /** Cleanly disposes a {@link Binary} even if it is null. */ - public static void closeQuietly(Binary binary) { - if (binary == null) - return; - binary.dispose(); - } - - /** Retrieve a {@link Binary} as a byte array */ - public static byte[] getBinaryAsBytes(Property property) { - try (ByteArrayOutputStream out = new ByteArrayOutputStream(); - Bin binary = new Bin(property); - InputStream in = binary.getStream()) { - IOUtils.copy(in, out); - return out.toByteArray(); - } catch (RepositoryException e) { - throw new JcrException("Cannot read binary " + property + " as bytes", e); - } catch (IOException e) { - throw new RuntimeException("Cannot read binary " + property + " as bytes", e); - } - } - - /** Writes a {@link Binary} from a byte array */ - public static void setBinaryAsBytes(Node node, String property, byte[] bytes) { - Binary binary = null; - try (InputStream in = new ByteArrayInputStream(bytes)) { - binary = node.getSession().getValueFactory().createBinary(in); - node.setProperty(property, binary); - } catch (RepositoryException e) { - throw new JcrException("Cannot set binary " + property + " as bytes", e); - } catch (IOException e) { - throw new RuntimeException("Cannot set binary " + property + " as bytes", e); - } finally { - closeQuietly(binary); - } - } - - /** Writes a {@link Binary} from a byte array */ - public static void setBinaryAsBytes(Property prop, byte[] bytes) { - Binary binary = null; - try (InputStream in = new ByteArrayInputStream(bytes)) { - binary = prop.getSession().getValueFactory().createBinary(in); - prop.setValue(binary); - } catch (RepositoryException e) { - throw new JcrException("Cannot set binary " + prop + " as bytes", e); - } catch (IOException e) { - throw new RuntimeException("Cannot set binary " + prop + " as bytes", e); - } finally { - closeQuietly(binary); - } - } - - /** - * Creates depth from a string (typically a username) by adding levels based on - * its first characters: "aBcD",2 becomes a/aB - */ - public static String firstCharsToPath(String str, Integer nbrOfChars) { - if (str.length() < nbrOfChars) - throw new IllegalArgumentException("String " + str + " length must be greater or equal than " + nbrOfChars); - StringBuffer path = new StringBuffer(""); - StringBuffer curr = new StringBuffer(""); - for (int i = 0; i < nbrOfChars; i++) { - curr.append(str.charAt(i)); - path.append(curr); - if (i < nbrOfChars - 1) - path.append('/'); - } - return path.toString(); - } - - /** - * Discards the current changes in the session attached to this node. To be used - * typically in a catch block. - * - * @see #discardQuietly(Session) - */ - public static void discardUnderlyingSessionQuietly(Node node) { - try { - discardQuietly(node.getSession()); - } catch (RepositoryException e) { - // silent - } - } - - /** - * Discards the current changes in a session by calling - * {@link Session#refresh(boolean)} with false, only logging - * potential errors when doing so. To be used typically in a catch block. - */ - public static void discardQuietly(Session session) { - try { - if (session != null) - session.refresh(false); - } catch (RepositoryException e) { - // silent - } - } - - /** - * Login to a workspace with implicit credentials, creates the workspace with - * these credentials if it does not already exist. - */ - public static Session loginOrCreateWorkspace(Repository repository, String workspaceName) - throws RepositoryException { - return loginOrCreateWorkspace(repository, workspaceName, null); - } - - /** - * Login to a workspace with implicit credentials, creates the workspace with - * these credentials if it does not already exist. - */ - public static Session loginOrCreateWorkspace(Repository repository, String workspaceName, Credentials credentials) - throws RepositoryException { - Session workspaceSession = null; - Session defaultSession = null; - try { - try { - workspaceSession = repository.login(credentials, workspaceName); - } catch (NoSuchWorkspaceException e) { - // try to create workspace - defaultSession = repository.login(credentials); - defaultSession.getWorkspace().createWorkspace(workspaceName); - workspaceSession = repository.login(credentials, workspaceName); - } - return workspaceSession; - } finally { - logoutQuietly(defaultSession); - } - } - - /** - * Logs out the session, not throwing any exception, even if it is null. - * {@link Jcr#logout(Session)} should rather be used. - */ - public static void logoutQuietly(Session session) { - Jcr.logout(session); -// try { -// if (session != null) -// if (session.isLive()) -// session.logout(); -// } catch (Exception e) { -// // silent -// } - } - - /** - * Convenient method to add a listener. uuids passed as null, deep=true, - * local=true, only one node type - */ - public static void addListener(Session session, EventListener listener, int eventTypes, String basePath, - String nodeType) { - try { - session.getWorkspace().getObservationManager().addEventListener(listener, eventTypes, basePath, true, null, - nodeType == null ? null : new String[] { nodeType }, true); - } catch (RepositoryException e) { - throw new JcrException("Cannot add JCR listener " + listener + " to session " + session, e); - } - } - - /** Removes a listener without throwing exception */ - public static void removeListenerQuietly(Session session, EventListener listener) { - if (session == null || !session.isLive()) - return; - try { - session.getWorkspace().getObservationManager().removeEventListener(listener); - } catch (RepositoryException e) { - // silent - } - } - - /** - * Quietly unregisters an {@link EventListener} from the udnerlying workspace of - * this node. - */ - public static void unregisterQuietly(Node node, EventListener eventListener) { - try { - unregisterQuietly(node.getSession().getWorkspace(), eventListener); - } catch (RepositoryException e) { - // silent - } - } - - /** Quietly unregisters an {@link EventListener} from this workspace */ - public static void unregisterQuietly(Workspace workspace, EventListener eventListener) { - if (eventListener == null) - return; - try { - workspace.getObservationManager().removeEventListener(eventListener); - } catch (RepositoryException e) { - // silent - } - } - - /** - * Checks whether {@link Property#JCR_LAST_MODIFIED} or (afterwards) - * {@link Property#JCR_CREATED} are set and returns it as an {@link Instant}. - */ - public static Instant getModified(Node node) { - Calendar calendar = null; - try { - if (node.hasProperty(Property.JCR_LAST_MODIFIED)) - calendar = node.getProperty(Property.JCR_LAST_MODIFIED).getDate(); - else if (node.hasProperty(Property.JCR_CREATED)) - calendar = node.getProperty(Property.JCR_CREATED).getDate(); - else - throw new IllegalArgumentException("No modification time found in " + node); - return calendar.toInstant(); - } catch (RepositoryException e) { - throw new JcrException("Cannot get modification time for " + node, e); - } - - } - - /** - * Get {@link Property#JCR_CREATED} as an {@link Instant}, if it is set. - */ - public static Instant getCreated(Node node) { - Calendar calendar = null; - try { - if (node.hasProperty(Property.JCR_CREATED)) - calendar = node.getProperty(Property.JCR_CREATED).getDate(); - else - throw new IllegalArgumentException("No created time found in " + node); - return calendar.toInstant(); - } catch (RepositoryException e) { - throw new JcrException("Cannot get created time for " + node, e); - } - - } - - /** - * Updates the {@link Property#JCR_LAST_MODIFIED} property with the current time - * and the {@link Property#JCR_LAST_MODIFIED_BY} property with the underlying - * session user id. - */ - public static void updateLastModified(Node node) { - updateLastModified(node, false); - } - - /** - * Updates the {@link Property#JCR_LAST_MODIFIED} property with the current time - * and the {@link Property#JCR_LAST_MODIFIED_BY} property with the underlying - * session user id. In Jackrabbit 2.x, - * these properties are - * not automatically updated, hence the need for manual update. The session - * is not saved. - */ - public static void updateLastModified(Node node, boolean addMixin) { - try { - if (addMixin && !node.isNodeType(NodeType.MIX_LAST_MODIFIED)) - node.addMixin(NodeType.MIX_LAST_MODIFIED); - node.setProperty(Property.JCR_LAST_MODIFIED, new GregorianCalendar()); - node.setProperty(Property.JCR_LAST_MODIFIED_BY, node.getSession().getUserID()); - } catch (RepositoryException e) { - throw new JcrException("Cannot update last modified on " + node, e); - } - } - - /** - * Update lastModified recursively until this parent. - * - * @param node the node - * @param untilPath the base path, null is equivalent to "/" - */ - public static void updateLastModifiedAndParents(Node node, String untilPath) { - updateLastModifiedAndParents(node, untilPath, true); - } - - /** - * Update lastModified recursively until this parent. - * - * @param node the node - * @param untilPath the base path, null is equivalent to "/" - */ - public static void updateLastModifiedAndParents(Node node, String untilPath, boolean addMixin) { - try { - if (untilPath != null && !node.getPath().startsWith(untilPath)) - throw new IllegalArgumentException(node + " is not under " + untilPath); - updateLastModified(node, addMixin); - if (untilPath == null) { - if (!node.getPath().equals("/")) - updateLastModifiedAndParents(node.getParent(), untilPath, addMixin); - } else { - if (!node.getPath().equals(untilPath)) - updateLastModifiedAndParents(node.getParent(), untilPath, addMixin); - } - } catch (RepositoryException e) { - throw new JcrException("Cannot update lastModified from " + node + " until " + untilPath, e); - } - } - - /** - * Returns a String representing the short version (see - * Node type - * Notation attributes grammar) of the main business attributes of this - * property definition - * - * @param prop - */ - public static String getPropertyDefinitionAsString(Property prop) { - StringBuffer sbuf = new StringBuffer(); - try { - if (prop.getDefinition().isAutoCreated()) - sbuf.append("a"); - if (prop.getDefinition().isMandatory()) - sbuf.append("m"); - if (prop.getDefinition().isProtected()) - sbuf.append("p"); - if (prop.getDefinition().isMultiple()) - sbuf.append("*"); - } catch (RepositoryException re) { - throw new JcrException("unexpected error while getting property definition as String", re); - } - return sbuf.toString(); - } - - /** - * Estimate the sub tree size from current node. Computation is based on the Jcr - * {@link Property#getLength()} method. Note : it is not the exact size used on - * the disk by the current part of the JCR Tree. - */ - - public static long getNodeApproxSize(Node node) { - long curNodeSize = 0; - try { - PropertyIterator pi = node.getProperties(); - while (pi.hasNext()) { - Property prop = pi.nextProperty(); - if (prop.isMultiple()) { - int nb = prop.getLengths().length; - for (int i = 0; i < nb; i++) { - curNodeSize += (prop.getLengths()[i] > 0 ? prop.getLengths()[i] : 0); - } - } else - curNodeSize += (prop.getLength() > 0 ? prop.getLength() : 0); - } - - NodeIterator ni = node.getNodes(); - while (ni.hasNext()) - curNodeSize += getNodeApproxSize(ni.nextNode()); - return curNodeSize; - } catch (RepositoryException re) { - throw new JcrException("Unexpected error while recursively determining node size.", re); - } - } - - /* - * SECURITY - */ - - /** - * Convenience method for adding a single privilege to a principal (user or - * role), typically jcr:all - */ - public synchronized static void addPrivilege(Session session, String path, String principal, String privilege) - throws RepositoryException { - List privileges = new ArrayList(); - privileges.add(session.getAccessControlManager().privilegeFromName(privilege)); - addPrivileges(session, path, new SimplePrincipal(principal), privileges); - } - - /** - * Add privileges on a path to a {@link Principal}. The path must already exist. - * Session is saved. Synchronized to prevent concurrent modifications of the - * same node. - */ - public synchronized static Boolean addPrivileges(Session session, String path, Principal principal, - List privs) throws RepositoryException { - // make sure the session is in line with the persisted state - session.refresh(false); - AccessControlManager acm = session.getAccessControlManager(); - AccessControlList acl = getAccessControlList(acm, path); - - accessControlEntries: for (AccessControlEntry ace : acl.getAccessControlEntries()) { - Principal currentPrincipal = ace.getPrincipal(); - if (currentPrincipal.getName().equals(principal.getName())) { - Privilege[] currentPrivileges = ace.getPrivileges(); - if (currentPrivileges.length != privs.size()) - break accessControlEntries; - for (int i = 0; i < currentPrivileges.length; i++) { - Privilege currP = currentPrivileges[i]; - Privilege p = privs.get(i); - if (!currP.getName().equals(p.getName())) { - break accessControlEntries; - } - } - return false; - } - } - - Privilege[] privileges = privs.toArray(new Privilege[privs.size()]); - acl.addAccessControlEntry(principal, privileges); - acm.setPolicy(path, acl); -// if (log.isDebugEnabled()) { -// StringBuffer privBuf = new StringBuffer(); -// for (Privilege priv : privs) -// privBuf.append(priv.getName()); -// log.debug("Added privileges " + privBuf + " to " + principal.getName() + " on " + path + " in '" -// + session.getWorkspace().getName() + "'"); -// } - session.refresh(true); - session.save(); - return true; - } - - /** - * Gets the first available access control list for this path, throws exception - * if not found - */ - public synchronized static AccessControlList getAccessControlList(AccessControlManager acm, String path) - throws RepositoryException { - // search for an access control list - AccessControlList acl = null; - AccessControlPolicyIterator policyIterator = acm.getApplicablePolicies(path); - applicablePolicies: if (policyIterator.hasNext()) { - while (policyIterator.hasNext()) { - AccessControlPolicy acp = policyIterator.nextAccessControlPolicy(); - if (acp instanceof AccessControlList) { - acl = ((AccessControlList) acp); - break applicablePolicies; - } - } - } else { - AccessControlPolicy[] existingPolicies = acm.getPolicies(path); - existingPolicies: for (AccessControlPolicy acp : existingPolicies) { - if (acp instanceof AccessControlList) { - acl = ((AccessControlList) acp); - break existingPolicies; - } - } - } - if (acl != null) - return acl; - else - throw new IllegalArgumentException("ACL not found at " + path); - } - - /** Clear authorizations for a user at this path */ - public synchronized static void clearAccessControList(Session session, String path, String username) - throws RepositoryException { - AccessControlManager acm = session.getAccessControlManager(); - AccessControlList acl = getAccessControlList(acm, path); - for (AccessControlEntry ace : acl.getAccessControlEntries()) { - if (ace.getPrincipal().getName().equals(username)) { - acl.removeAccessControlEntry(ace); - } - } - // the new access control list must be applied otherwise this call: - // acl.removeAccessControlEntry(ace); has no effect - acm.setPolicy(path, acl); - session.refresh(true); - session.save(); - } - - /* - * FILES UTILITIES - */ - /** - * Creates the nodes making the path as {@link NodeType#NT_FOLDER} - */ - public static Node mkfolders(Session session, String path) { - return mkdirs(session, path, NodeType.NT_FOLDER, NodeType.NT_FOLDER, false); - } - - /** - * Copy only nt:folder and nt:file, without their additional types and - * properties. - * - * @param recursive if true copies folders as well, otherwise only first level - * files - * @return how many files were copied - */ - public static Long copyFiles(Node fromNode, Node toNode, Boolean recursive, JcrMonitor monitor, boolean onlyAdd) { - long count = 0l; - - // Binary binary = null; - // InputStream in = null; - try { - NodeIterator fromChildren = fromNode.getNodes(); - children: while (fromChildren.hasNext()) { - if (monitor != null && monitor.isCanceled()) - throw new IllegalStateException("Copy cancelled before it was completed"); - - Node fromChild = fromChildren.nextNode(); - String fileName = fromChild.getName(); - if (fromChild.isNodeType(NodeType.NT_FILE)) { - if (onlyAdd && toNode.hasNode(fileName)) { - monitor.subTask("Skip existing " + fileName); - continue children; - } - - if (monitor != null) - monitor.subTask("Copy " + fileName); - try (Bin binary = new Bin(fromChild.getNode(Node.JCR_CONTENT).getProperty(Property.JCR_DATA)); - InputStream in = binary.getStream();) { - copyStreamAsFile(toNode, fileName, in); - } catch (IOException e) { - throw new RuntimeException("Cannot copy " + fileName + " to " + toNode, e); - } - - // save session - toNode.getSession().save(); - count++; - -// if (log.isDebugEnabled()) -// log.debug("Copied file " + fromChild.getPath()); - if (monitor != null) - monitor.worked(1); - } else if (fromChild.isNodeType(NodeType.NT_FOLDER) && recursive) { - Node toChildFolder; - if (toNode.hasNode(fileName)) { - toChildFolder = toNode.getNode(fileName); - if (!toChildFolder.isNodeType(NodeType.NT_FOLDER)) - throw new IllegalArgumentException(toChildFolder + " is not of type nt:folder"); - } else { - toChildFolder = toNode.addNode(fileName, NodeType.NT_FOLDER); - - // save session - toNode.getSession().save(); - } - count = count + copyFiles(fromChild, toChildFolder, recursive, monitor, onlyAdd); - } - } - return count; - } catch (RepositoryException e) { - throw new JcrException("Cannot copy files between " + fromNode + " and " + toNode, e); - } finally { - // in case there was an exception - // IOUtils.closeQuietly(in); - // closeQuietly(binary); - } - } - - /** - * Iteratively count all file nodes in subtree, inefficient but can be useful - * when query are poorly supported, such as in remoting. - */ - public static Long countFiles(Node node) { - Long localCount = 0l; - try { - for (NodeIterator nit = node.getNodes(); nit.hasNext();) { - Node child = nit.nextNode(); - if (child.isNodeType(NodeType.NT_FOLDER)) - localCount = localCount + countFiles(child); - else if (child.isNodeType(NodeType.NT_FILE)) - localCount = localCount + 1; - } - } catch (RepositoryException e) { - throw new JcrException("Cannot count all children of " + node, e); - } - return localCount; - } - - /** - * Copy a file as an nt:file, assuming an nt:folder hierarchy. The session is - * NOT saved. - * - * @return the created file node - */ - @Deprecated - public static Node copyFile(Node folderNode, File file) { - try (InputStream in = new FileInputStream(file)) { - return copyStreamAsFile(folderNode, file.getName(), in); - } catch (IOException e) { - throw new RuntimeException("Cannot copy file " + file + " under " + folderNode, e); - } - } - - /** Copy bytes as an nt:file */ - public static Node copyBytesAsFile(Node folderNode, String fileName, byte[] bytes) { - // InputStream in = null; - try (InputStream in = new ByteArrayInputStream(bytes)) { - // in = new ByteArrayInputStream(bytes); - return copyStreamAsFile(folderNode, fileName, in); - } catch (IOException e) { - throw new RuntimeException("Cannot copy file " + fileName + " under " + folderNode, e); - // } finally { - // IOUtils.closeQuietly(in); - } - } - - /** - * Copy a stream as an nt:file, assuming an nt:folder hierarchy. The session is - * NOT saved. - * - * @return the created file node - */ - public static Node copyStreamAsFile(Node folderNode, String fileName, InputStream in) { - Binary binary = null; - try { - Node fileNode; - Node contentNode; - if (folderNode.hasNode(fileName)) { - fileNode = folderNode.getNode(fileName); - if (!fileNode.isNodeType(NodeType.NT_FILE)) - throw new IllegalArgumentException(fileNode + " is not of type nt:file"); - // we assume that the content node is already there - contentNode = fileNode.getNode(Node.JCR_CONTENT); - } else { - fileNode = folderNode.addNode(fileName, NodeType.NT_FILE); - contentNode = fileNode.addNode(Node.JCR_CONTENT, NodeType.NT_UNSTRUCTURED); - } - binary = contentNode.getSession().getValueFactory().createBinary(in); - contentNode.setProperty(Property.JCR_DATA, binary); - updateLastModified(contentNode); - return fileNode; - } catch (RepositoryException e) { - throw new JcrException("Cannot create file node " + fileName + " under " + folderNode, e); - } finally { - closeQuietly(binary); - } - } - - /** Read an an nt:file as an {@link InputStream}. */ - public static InputStream getFileAsStream(Node fileNode) throws RepositoryException { - return fileNode.getNode(Node.JCR_CONTENT).getProperty(Property.JCR_DATA).getBinary().getStream(); - } - - /** - * Set the properties of {@link NodeType#MIX_MIMETYPE} on the content of this - * file node. - */ - public static void setFileMimeType(Node fileNode, String mimeType, String encoding) throws RepositoryException { - Node contentNode = fileNode.getNode(Node.JCR_CONTENT); - if (mimeType != null) - contentNode.setProperty(Property.JCR_MIMETYPE, mimeType); - if (encoding != null) - contentNode.setProperty(Property.JCR_ENCODING, encoding); - // TODO remove properties if args are null? - } - - public static void copyFilesToFs(Node baseNode, Path targetDir, boolean recursive) { - try { - Files.createDirectories(targetDir); - for (NodeIterator nit = baseNode.getNodes(); nit.hasNext();) { - Node node = nit.nextNode(); - if (node.isNodeType(NodeType.NT_FILE)) { - Path filePath = targetDir.resolve(node.getName()); - try (OutputStream out = Files.newOutputStream(filePath); InputStream in = getFileAsStream(node)) { - IOUtils.copy(in, out); - } - } else if (recursive && node.isNodeType(NodeType.NT_FOLDER)) { - Path dirPath = targetDir.resolve(node.getName()); - copyFilesToFs(node, dirPath, true); - } - } - } catch (RepositoryException e) { - throw new JcrException("Cannot copy " + baseNode + " to " + targetDir, e); - } catch (IOException e) { - throw new RuntimeException("Cannot copy " + baseNode + " to " + targetDir, e); - } - } - - /** - * Computes the checksum of an nt:file. - * - * @deprecated use separate digest utilities - */ - @Deprecated - public static String checksumFile(Node fileNode, String algorithm) { - try (InputStream in = fileNode.getNode(Node.JCR_CONTENT).getProperty(Property.JCR_DATA).getBinary() - .getStream()) { - return digest(algorithm, in); - } catch (IOException e) { - throw new RuntimeException("Cannot checksum file " + fileNode + " with algorithm " + algorithm, e); - } catch (RepositoryException e) { - throw new JcrException("Cannot checksum file " + fileNode + " with algorithm " + algorithm, e); - } - } - - @Deprecated - private static String digest(String algorithm, InputStream in) { - final Integer byteBufferCapacity = 100 * 1024;// 100 KB - try { - MessageDigest digest = MessageDigest.getInstance(algorithm); - byte[] buffer = new byte[byteBufferCapacity]; - int read = 0; - while ((read = in.read(buffer)) > 0) { - digest.update(buffer, 0, read); - } - - byte[] checksum = digest.digest(); - String res = encodeHexString(checksum); - return res; - } catch (IOException e) { - throw new RuntimeException("Cannot digest with algorithm " + algorithm, e); - } catch (NoSuchAlgorithmException e) { - throw new IllegalArgumentException("Cannot digest with algorithm " + algorithm, e); - } - } - - /** - * From - * http://stackoverflow.com/questions/9655181/how-to-convert-a-byte-array-to - * -a-hex-string-in-java - */ - @Deprecated - private static String encodeHexString(byte[] bytes) { - final char[] hexArray = "0123456789abcdef".toCharArray(); - char[] hexChars = new char[bytes.length * 2]; - for (int j = 0; j < bytes.length; j++) { - int v = bytes[j] & 0xFF; - hexChars[j * 2] = hexArray[v >>> 4]; - hexChars[j * 2 + 1] = hexArray[v & 0x0F]; - } - return new String(hexChars); - } - - /** Export a subtree as a compact XML without namespaces. */ - public static void toSimpleXml(Node node, StringBuilder sb) throws RepositoryException { - sb.append('<'); - String nodeName = node.getName(); - int colIndex = nodeName.indexOf(':'); - if (colIndex > 0) { - nodeName = nodeName.substring(colIndex + 1); - } - sb.append(nodeName); - PropertyIterator pit = node.getProperties(); - properties: while (pit.hasNext()) { - Property p = pit.nextProperty(); - // skip multiple properties - if (p.isMultiple()) - continue properties; - String propertyName = p.getName(); - int pcolIndex = propertyName.indexOf(':'); - // skip properties with namespaces - if (pcolIndex > 0) - continue properties; - // skip binaries - if (p.getType() == PropertyType.BINARY) { - continue properties; - // TODO retrieve identifier? - } - sb.append(' '); - sb.append(propertyName); - sb.append('='); - sb.append('\"').append(p.getString()).append('\"'); - } - - if (node.hasNodes()) { - sb.append('>'); - NodeIterator children = node.getNodes(); - while (children.hasNext()) { - toSimpleXml(children.nextNode(), sb); - } - sb.append("'); - } else { - sb.append("/>"); - } - } - -} diff --git a/org.argeo.jcr/src/org/argeo/jcr/JcrxApi.java b/org.argeo.jcr/src/org/argeo/jcr/JcrxApi.java deleted file mode 100644 index 666b2593e..000000000 --- a/org.argeo.jcr/src/org/argeo/jcr/JcrxApi.java +++ /dev/null @@ -1,190 +0,0 @@ -package org.argeo.jcr; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; - -import javax.jcr.Node; -import javax.jcr.Property; -import javax.jcr.RepositoryException; -import javax.jcr.Value; - -/** Uilities around the JCR extensions. */ -public class JcrxApi { - public final static String MD5 = "MD5"; - public final static String SHA1 = "SHA1"; - public final static String SHA256 = "SHA-256"; - public final static String SHA512 = "SHA-512"; - - public final static String EMPTY_MD5 = "d41d8cd98f00b204e9800998ecf8427e"; - public final static String EMPTY_SHA1 = "da39a3ee5e6b4b0d3255bfef95601890afd80709"; - public final static String EMPTY_SHA256 = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"; - public final static String EMPTY_SHA512 = "cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e"; - - public final static int LENGTH_MD5 = EMPTY_MD5.length(); - public final static int LENGTH_SHA1 = EMPTY_SHA1.length(); - public final static int LENGTH_SHA256 = EMPTY_SHA256.length(); - public final static int LENGTH_SHA512 = EMPTY_SHA512.length(); - - /* - * XML - */ - /** - * Get the XML text of this child node. - */ - public static String getXmlValue(Node node, String name) { - try { - if (!node.hasNode(name)) - return null; - Node child = node.getNode(name); - return getXmlValue(child); - } catch (RepositoryException e) { - throw new IllegalStateException("Cannot get " + name + " as XML text", e); - } - } - - /** - * Get the XML text of this node. - */ - public static String getXmlValue(Node node) { - try { - if (!node.hasNode(Jcr.JCR_XMLTEXT)) - return null; - Node xmlText = node.getNode(Jcr.JCR_XMLTEXT); - if (!xmlText.hasProperty(Jcr.JCR_XMLCHARACTERS)) - throw new IllegalArgumentException( - "Node " + xmlText + " has no " + Jcr.JCR_XMLCHARACTERS + " property"); - return xmlText.getProperty(Jcr.JCR_XMLCHARACTERS).getString(); - } catch (RepositoryException e) { - throw new IllegalStateException("Cannot get " + node + " as XML text", e); - } - } - - /** - * Set as a subnode which will be exported as an XML element. - */ - public static void setXmlValue(Node node, String name, String value) { - try { - if (node.hasNode(name)) { - Node child = node.getNode(name); - setXmlValue(node, child, value); - } else - node.addNode(name, JcrxType.JCRX_XMLVALUE).addNode(Jcr.JCR_XMLTEXT, JcrxType.JCRX_XMLTEXT) - .setProperty(Jcr.JCR_XMLCHARACTERS, value); - } catch (RepositoryException e) { - throw new JcrException("Cannot set " + name + " as XML text", e); - } - } - - public static void setXmlValue(Node node, Node child, String value) { - try { - if (!child.hasNode(Jcr.JCR_XMLTEXT)) - child.addNode(Jcr.JCR_XMLTEXT, JcrxType.JCRX_XMLTEXT); - child.getNode(Jcr.JCR_XMLTEXT).setProperty(Jcr.JCR_XMLCHARACTERS, value); - } catch (RepositoryException e) { - throw new JcrException("Cannot set " + child + " as XML text", e); - } - } - - /** - * Add a checksum replacing the one which was previously set with the same - * length. - */ - public static void addChecksum(Node node, String checksum) { - try { - if (!node.hasProperty(JcrxName.JCRX_SUM)) { - node.setProperty(JcrxName.JCRX_SUM, new String[] { checksum }); - return; - } else { - int stringLength = checksum.length(); - Property property = node.getProperty(JcrxName.JCRX_SUM); - List values = Arrays.asList(property.getValues()); - Integer indexToRemove = null; - values: for (int i = 0; i < values.size(); i++) { - Value value = values.get(i); - if (value.getString().length() == stringLength) { - indexToRemove = i; - break values; - } - } - if (indexToRemove != null) - values.set(indexToRemove, node.getSession().getValueFactory().createValue(checksum)); - else - values.add(0, node.getSession().getValueFactory().createValue(checksum)); - property.setValue(values.toArray(new Value[values.size()])); - } - } catch (RepositoryException e) { - throw new JcrException("Cannot set checksum on " + node, e); - } - } - - /** Replace all checksums. */ - public static void setChecksums(Node node, List checksums) { - try { - node.setProperty(JcrxName.JCRX_SUM, checksums.toArray(new String[checksums.size()])); - } catch (RepositoryException e) { - throw new JcrException("Cannot set checksums on " + node, e); - } - } - - /** Replace all checksums. */ - public static List getChecksums(Node node) { - try { - List res = new ArrayList<>(); - if (!node.hasProperty(JcrxName.JCRX_SUM)) - return res; - Property property = node.getProperty(JcrxName.JCRX_SUM); - for (Value value : property.getValues()) { - res.add(value.getString()); - } - return res; - } catch (RepositoryException e) { - throw new JcrException("Cannot get checksums from " + node, e); - } - } - -// /** Replace all checksums with this single one. */ -// public static void setChecksum(Node node, String checksum) { -// setChecksums(node, Collections.singletonList(checksum)); -// } - - /** Retrieves the checksum with this algorithm, or null if not found. */ - public static String getChecksum(Node node, String algorithm) { - int stringLength; - switch (algorithm) { - case MD5: - stringLength = LENGTH_MD5; - break; - case SHA1: - stringLength = LENGTH_SHA1; - break; - case SHA256: - stringLength = LENGTH_SHA256; - break; - case SHA512: - stringLength = LENGTH_SHA512; - break; - default: - throw new IllegalArgumentException("Unkown algorithm " + algorithm); - } - return getChecksum(node, stringLength); - } - - /** Retrieves the checksum with this string length, or null if not found. */ - public static String getChecksum(Node node, int stringLength) { - try { - if (!node.hasProperty(JcrxName.JCRX_SUM)) - return null; - Property property = node.getProperty(JcrxName.JCRX_SUM); - for (Value value : property.getValues()) { - String str = value.getString(); - if (str.length() == stringLength) - return str; - } - return null; - } catch (RepositoryException e) { - throw new IllegalStateException("Cannot get checksum for " + node, e); - } - } - -} diff --git a/org.argeo.jcr/src/org/argeo/jcr/JcrxName.java b/org.argeo.jcr/src/org/argeo/jcr/JcrxName.java deleted file mode 100644 index 9dd43adce..000000000 --- a/org.argeo.jcr/src/org/argeo/jcr/JcrxName.java +++ /dev/null @@ -1,7 +0,0 @@ -package org.argeo.jcr; - -/** Names declared by the JCR extensions. */ -public interface JcrxName { - /** The multiple property holding various coherent checksums. */ - public final static String JCRX_SUM = "{http://www.argeo.org/ns/jcrx}sum"; -} diff --git a/org.argeo.jcr/src/org/argeo/jcr/JcrxType.java b/org.argeo.jcr/src/org/argeo/jcr/JcrxType.java deleted file mode 100644 index 0cbad3341..000000000 --- a/org.argeo.jcr/src/org/argeo/jcr/JcrxType.java +++ /dev/null @@ -1,17 +0,0 @@ -package org.argeo.jcr; - -/** Node types declared by the JCR extensions. */ -public interface JcrxType { - /** - * Node type for an XML value, which will be serialized in XML as an element - * containing text. - */ - public final static String JCRX_XMLVALUE = "{http://www.argeo.org/ns/jcrx}xmlvalue"; - - /** Node type for the node containing the text. */ - public final static String JCRX_XMLTEXT = "{http://www.argeo.org/ns/jcrx}xmltext"; - - /** Mixin node type for a set of checksums. */ - public final static String JCRX_CSUM = "{http://www.argeo.org/ns/jcrx}csum"; - -} diff --git a/org.argeo.jcr/src/org/argeo/jcr/PropertyDiff.java b/org.argeo.jcr/src/org/argeo/jcr/PropertyDiff.java deleted file mode 100644 index 71e76fe9b..000000000 --- a/org.argeo.jcr/src/org/argeo/jcr/PropertyDiff.java +++ /dev/null @@ -1,57 +0,0 @@ -package org.argeo.jcr; - -import javax.jcr.Value; - -/** The result of the comparison of two JCR properties. */ -public class PropertyDiff { - public final static Integer MODIFIED = 0; - public final static Integer ADDED = 1; - public final static Integer REMOVED = 2; - - private final Integer type; - private final String relPath; - private final Value referenceValue; - private final Value newValue; - - public PropertyDiff(Integer type, String relPath, Value referenceValue, Value newValue) { - super(); - - if (type == MODIFIED) { - if (referenceValue == null || newValue == null) - throw new IllegalArgumentException("Reference and new values must be specified."); - } else if (type == ADDED) { - if (referenceValue != null || newValue == null) - throw new IllegalArgumentException("New value and only it must be specified."); - } else if (type == REMOVED) { - if (referenceValue == null || newValue != null) - throw new IllegalArgumentException("Reference value and only it must be specified."); - } else { - throw new IllegalArgumentException("Unkown diff type " + type); - } - - if (relPath == null) - throw new IllegalArgumentException("Relative path must be specified"); - - this.type = type; - this.relPath = relPath; - this.referenceValue = referenceValue; - this.newValue = newValue; - } - - public Integer getType() { - return type; - } - - public String getRelPath() { - return relPath; - } - - public Value getReferenceValue() { - return referenceValue; - } - - public Value getNewValue() { - return newValue; - } - -} diff --git a/org.argeo.jcr/src/org/argeo/jcr/SimplePrincipal.java b/org.argeo.jcr/src/org/argeo/jcr/SimplePrincipal.java deleted file mode 100644 index 4f42f2d9c..000000000 --- a/org.argeo.jcr/src/org/argeo/jcr/SimplePrincipal.java +++ /dev/null @@ -1,43 +0,0 @@ -package org.argeo.jcr; - -import java.security.Principal; - -/** Canonical implementation of a {@link Principal} */ -class SimplePrincipal implements Principal { - private final String name; - - public SimplePrincipal(String name) { - if (name == null) - throw new IllegalArgumentException("Principal name cannot be null"); - this.name = name; - } - - public String getName() { - return name; - } - - @Override - public int hashCode() { - return name.hashCode(); - } - - @Override - public boolean equals(Object obj) { - if (obj == null) - return false; - if (obj instanceof Principal) - return name.equals((((Principal) obj).getName())); - return name.equals(obj.toString()); - } - - @Override - protected Object clone() throws CloneNotSupportedException { - return new SimplePrincipal(name); - } - - @Override - public String toString() { - return name; - } - -} diff --git a/org.argeo.jcr/src/org/argeo/jcr/ThreadBoundJcrSessionFactory.java b/org.argeo.jcr/src/org/argeo/jcr/ThreadBoundJcrSessionFactory.java deleted file mode 100644 index 1e23338b5..000000000 --- a/org.argeo.jcr/src/org/argeo/jcr/ThreadBoundJcrSessionFactory.java +++ /dev/null @@ -1,280 +0,0 @@ -package org.argeo.jcr; - -import java.lang.reflect.InvocationHandler; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.lang.reflect.Proxy; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.Iterator; -import java.util.List; -import java.util.Map; - -import javax.jcr.LoginException; -import javax.jcr.Repository; -import javax.jcr.RepositoryException; -import javax.jcr.Session; -import javax.jcr.SimpleCredentials; - -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; - -/** Proxy JCR sessions and attach them to calling threads. */ -@Deprecated -public abstract class ThreadBoundJcrSessionFactory { - private final static Log log = LogFactory.getLog(ThreadBoundJcrSessionFactory.class); - - private Repository repository; - /** can be injected as list, only used if repository is null */ - private List repositories; - - private ThreadLocal session = new ThreadLocal(); - private final Session proxiedSession; - /** If workspace is null, default will be used. */ - private String workspace = null; - - private String defaultUsername = "demo"; - private String defaultPassword = "demo"; - private Boolean forceDefaultCredentials = false; - - private boolean active = true; - - // monitoring - private final List threads = Collections.synchronizedList(new ArrayList()); - private final Map activeSessions = Collections.synchronizedMap(new HashMap()); - private MonitoringThread monitoringThread; - - public ThreadBoundJcrSessionFactory() { - Class[] interfaces = { Session.class }; - proxiedSession = (Session) Proxy.newProxyInstance(ThreadBoundJcrSessionFactory.class.getClassLoader(), - interfaces, new JcrSessionInvocationHandler()); - } - - /** Logs in to the repository using various strategies. */ - protected synchronized Session login() { - if (!isActive()) - throw new IllegalStateException("Thread bound session factory inactive"); - - // discard session previously attached to this thread - Thread thread = Thread.currentThread(); - if (activeSessions.containsKey(thread.getId())) { - Session oldSession = activeSessions.remove(thread.getId()); - oldSession.logout(); - session.remove(); - } - - Session newSession = null; - // first try to login without credentials, assuming the underlying login - // module will have dealt with authentication (typically using Spring - // Security) - if (!forceDefaultCredentials) - try { - newSession = repository().login(workspace); - } catch (LoginException e1) { - log.warn("Cannot login without credentials: " + e1.getMessage()); - // invalid credentials, go to the next step - } catch (RepositoryException e1) { - // other kind of exception, fail - throw new JcrException("Cannot log in to repository", e1); - } - - // log using default username / password (useful for testing purposes) - if (newSession == null) - try { - SimpleCredentials sc = new SimpleCredentials(defaultUsername, defaultPassword.toCharArray()); - newSession = repository().login(sc, workspace); - } catch (RepositoryException e) { - throw new JcrException("Cannot log in to repository", e); - } - - session.set(newSession); - // Log and monitor new session - if (log.isTraceEnabled()) - log.trace("Logged in to JCR session " + newSession + "; userId=" + newSession.getUserID()); - - // monitoring - activeSessions.put(thread.getId(), newSession); - threads.add(thread); - return newSession; - } - - public Object getObject() { - return proxiedSession; - } - - public void init() throws Exception { - // log.error("SHOULD NOT BE USED ANYMORE"); - monitoringThread = new MonitoringThread(); - monitoringThread.start(); - } - - public void dispose() throws Exception { - // if (activeSessions.size() == 0) - // return; - - if (log.isTraceEnabled()) - log.trace("Cleaning up " + activeSessions.size() + " active JCR sessions..."); - - deactivate(); - for (Session sess : activeSessions.values()) { - JcrUtils.logoutQuietly(sess); - } - activeSessions.clear(); - } - - protected Boolean isActive() { - return active; - } - - protected synchronized void deactivate() { - active = false; - notifyAll(); - } - - protected synchronized void removeSession(Thread thread) { - if (!isActive()) - return; - activeSessions.remove(thread.getId()); - threads.remove(thread); - } - - protected synchronized void cleanDeadThreads() { - if (!isActive()) - return; - Iterator it = threads.iterator(); - while (it.hasNext()) { - Thread thread = it.next(); - if (!thread.isAlive() && isActive()) { - if (activeSessions.containsKey(thread.getId())) { - Session session = activeSessions.get(thread.getId()); - activeSessions.remove(thread.getId()); - session.logout(); - if (log.isTraceEnabled()) - log.trace("Cleaned up JCR session (userID=" + session.getUserID() + ") from dead thread " - + thread.getId()); - } - it.remove(); - } - } - try { - wait(1000); - } catch (InterruptedException e) { - // silent - } - } - - public Class getObjectType() { - return Session.class; - } - - public boolean isSingleton() { - return true; - } - - /** - * Called before a method is actually called, allowing to check the session or - * re-login it (e.g. if authentication has changed). The default implementation - * returns the session. - */ - protected Session preCall(Session session) { - return session; - } - - protected Repository repository() { - if (repository != null) - return repository; - if (repositories != null) { - // hardened for OSGi dynamic services - Iterator it = repositories.iterator(); - if (it.hasNext()) - return it.next(); - } - throw new IllegalStateException("No repository injected"); - } - - // /** Useful for declarative registration of OSGi services (blueprint) */ - // public void register(Repository repository, Map params) { - // this.repository = repository; - // } - // - // /** Useful for declarative registration of OSGi services (blueprint) */ - // public void unregister(Repository repository, Map params) { - // this.repository = null; - // } - - public void setRepository(Repository repository) { - this.repository = repository; - } - - public void setRepositories(List repositories) { - this.repositories = repositories; - } - - public void setDefaultUsername(String defaultUsername) { - this.defaultUsername = defaultUsername; - } - - public void setDefaultPassword(String defaultPassword) { - this.defaultPassword = defaultPassword; - } - - public void setForceDefaultCredentials(Boolean forceDefaultCredentials) { - this.forceDefaultCredentials = forceDefaultCredentials; - } - - public void setWorkspace(String workspace) { - this.workspace = workspace; - } - - protected class JcrSessionInvocationHandler implements InvocationHandler { - - public Object invoke(Object proxy, Method method, Object[] args) throws Throwable, RepositoryException { - Session threadSession = session.get(); - if (threadSession == null) { - if ("logout".equals(method.getName()))// no need to login - return Void.TYPE; - else if ("toString".equals(method.getName()))// maybe logging - return "Uninitialized Argeo thread bound JCR session"; - threadSession = login(); - } - - preCall(threadSession); - Object ret; - try { - ret = method.invoke(threadSession, args); - } catch (InvocationTargetException e) { - Throwable cause = e.getCause(); - if (cause instanceof RepositoryException) - throw (RepositoryException) cause; - else - throw cause; - } - if ("logout".equals(method.getName())) { - session.remove(); - Thread thread = Thread.currentThread(); - removeSession(thread); - if (log.isTraceEnabled()) - log.trace("Logged out JCR session (userId=" + threadSession.getUserID() + ") on thread " - + thread.getId()); - } - return ret; - } - } - - /** Monitors registered thread in order to clean up dead ones. */ - private class MonitoringThread extends Thread { - - public MonitoringThread() { - super("ThreadBound JCR Session Monitor"); - } - - @Override - public void run() { - while (isActive()) { - cleanDeadThreads(); - } - } - - } -} diff --git a/org.argeo.jcr/src/org/argeo/jcr/VersionDiff.java b/org.argeo.jcr/src/org/argeo/jcr/VersionDiff.java deleted file mode 100644 index dab55548b..000000000 --- a/org.argeo.jcr/src/org/argeo/jcr/VersionDiff.java +++ /dev/null @@ -1,38 +0,0 @@ -package org.argeo.jcr; - -import java.util.Calendar; -import java.util.Map; - -/** - * Generic Object that enables the creation of history reports based on a JCR - * versionable node. userId and creation date are added to the map of - * PropertyDiff. - * - * These two fields might be null - * - */ -public class VersionDiff { - - private String userId; - private Map diffs; - private Calendar updateTime; - - public VersionDiff(String userId, Calendar updateTime, - Map diffs) { - this.userId = userId; - this.updateTime = updateTime; - this.diffs = diffs; - } - - public String getUserId() { - return userId; - } - - public Map getDiffs() { - return diffs; - } - - public Calendar getUpdateTime() { - return updateTime; - } -} diff --git a/org.argeo.jcr/src/org/argeo/jcr/fs/BinaryChannel.java b/org.argeo.jcr/src/org/argeo/jcr/fs/BinaryChannel.java deleted file mode 100644 index d6550feee..000000000 --- a/org.argeo.jcr/src/org/argeo/jcr/fs/BinaryChannel.java +++ /dev/null @@ -1,190 +0,0 @@ -package org.argeo.jcr.fs; - -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.nio.ByteBuffer; -import java.nio.channels.Channels; -import java.nio.channels.FileChannel; -import java.nio.channels.ReadableByteChannel; -import java.nio.channels.SeekableByteChannel; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.StandardOpenOption; - -import javax.jcr.Binary; -import javax.jcr.Node; -import javax.jcr.Property; -import javax.jcr.RepositoryException; -import javax.jcr.Session; -import javax.jcr.nodetype.NodeType; - -import org.argeo.jcr.JcrUtils; - -/** A read/write {@link SeekableByteChannel} based on a {@link Binary}. */ -public class BinaryChannel implements SeekableByteChannel { - private final Node file; - private Binary binary; - private boolean open = true; - - private long position = 0; - - private FileChannel fc = null; - - public BinaryChannel(Node file, Path path) throws RepositoryException, IOException { - this.file = file; - Session session = file.getSession(); - synchronized (session) { - if (file.isNodeType(NodeType.NT_FILE)) { - if (file.hasNode(Node.JCR_CONTENT)) { - Node data = file.getNode(Property.JCR_CONTENT); - this.binary = data.getProperty(Property.JCR_DATA).getBinary(); - } else { - Node data = file.addNode(Node.JCR_CONTENT, NodeType.NT_UNSTRUCTURED); - data.addMixin(NodeType.MIX_LAST_MODIFIED); - try (InputStream in = new ByteArrayInputStream(new byte[0])) { - this.binary = data.getSession().getValueFactory().createBinary(in); - } - data.setProperty(Property.JCR_DATA, this.binary); - - // MIME type - String mime = Files.probeContentType(path); - // String mime = fileTypeMap.getContentType(file.getName()); - data.setProperty(Property.JCR_MIMETYPE, mime); - - session.refresh(true); - session.save(); - session.notifyAll(); - } - } else { - throw new IllegalArgumentException( - "Unsupported file node " + file + " (" + file.getPrimaryNodeType() + ")"); - } - } - } - - @Override - public synchronized boolean isOpen() { - return open; - } - - @Override - public synchronized void close() throws IOException { - if (isModified()) { - Binary newBinary = null; - try { - Session session = file.getSession(); - synchronized (session) { - fc.position(0); - InputStream in = Channels.newInputStream(fc); - newBinary = session.getValueFactory().createBinary(in); - file.getNode(Property.JCR_CONTENT).setProperty(Property.JCR_DATA, newBinary); - session.refresh(true); - session.save(); - open = false; - session.notifyAll(); - } - } catch (RepositoryException e) { - throw new IOException("Cannot close " + file, e); - } finally { - JcrUtils.closeQuietly(newBinary); - // IOUtils.closeQuietly(fc); - if (fc != null) { - fc.close(); - } - } - } else { - clearReadState(); - open = false; - } - } - - @Override - public int read(ByteBuffer dst) throws IOException { - if (isModified()) { - return fc.read(dst); - } else { - - try { - int read; - byte[] arr = dst.array(); - read = binary.read(arr, position); - - if (read != -1) - position = position + read; - return read; - } catch (RepositoryException e) { - throw new IOException("Cannot read into buffer", e); - } - } - } - - @Override - public int write(ByteBuffer src) throws IOException { - int written = getFileChannel().write(src); - return written; - } - - @Override - public long position() throws IOException { - if (isModified()) - return getFileChannel().position(); - else - return position; - } - - @Override - public SeekableByteChannel position(long newPosition) throws IOException { - if (isModified()) { - getFileChannel().position(position); - } else { - this.position = newPosition; - } - return this; - } - - @Override - public long size() throws IOException { - if (isModified()) { - return getFileChannel().size(); - } else { - try { - return binary.getSize(); - } catch (RepositoryException e) { - throw new IOException("Cannot get size", e); - } - } - } - - @Override - public SeekableByteChannel truncate(long size) throws IOException { - getFileChannel().truncate(size); - return this; - } - - private FileChannel getFileChannel() throws IOException { - try { - if (fc == null) { - Path tempPath = Files.createTempFile(getClass().getSimpleName(), null); - fc = FileChannel.open(tempPath, StandardOpenOption.WRITE, StandardOpenOption.READ, - StandardOpenOption.DELETE_ON_CLOSE, StandardOpenOption.SPARSE); - ReadableByteChannel readChannel = Channels.newChannel(binary.getStream()); - fc.transferFrom(readChannel, 0, binary.getSize()); - clearReadState(); - } - return fc; - } catch (RepositoryException e) { - throw new IOException("Cannot get temp file channel", e); - } - } - - private boolean isModified() { - return fc != null; - } - - private void clearReadState() { - position = -1; - JcrUtils.closeQuietly(binary); - binary = null; - } -} diff --git a/org.argeo.jcr/src/org/argeo/jcr/fs/JcrBasicfileAttributes.java b/org.argeo.jcr/src/org/argeo/jcr/fs/JcrBasicfileAttributes.java deleted file mode 100644 index 7c9711bf0..000000000 --- a/org.argeo.jcr/src/org/argeo/jcr/fs/JcrBasicfileAttributes.java +++ /dev/null @@ -1,138 +0,0 @@ -package org.argeo.jcr.fs; - -import static javax.jcr.Property.JCR_CREATED; -import static javax.jcr.Property.JCR_LAST_MODIFIED; - -import java.nio.file.attribute.FileTime; -import java.time.Instant; - -import javax.jcr.Binary; -import javax.jcr.Node; -import javax.jcr.Property; -import javax.jcr.RepositoryException; -import javax.jcr.nodetype.NodeType; - -import org.argeo.jcr.JcrUtils; - -public class JcrBasicfileAttributes implements NodeFileAttributes { - private final Node node; - - private final static FileTime EPOCH = FileTime.fromMillis(0); - - public JcrBasicfileAttributes(Node node) { - if (node == null) - throw new JcrFsException("Node underlying the attributes cannot be null"); - this.node = node; - } - - @Override - public FileTime lastModifiedTime() { - try { - if (node.hasProperty(JCR_LAST_MODIFIED)) { - Instant instant = node.getProperty(JCR_LAST_MODIFIED).getDate().toInstant(); - return FileTime.from(instant); - } else if (node.hasProperty(JCR_CREATED)) { - Instant instant = node.getProperty(JCR_CREATED).getDate().toInstant(); - return FileTime.from(instant); - } -// if (node.isNodeType(NodeType.MIX_LAST_MODIFIED)) { -// Instant instant = node.getProperty(Property.JCR_LAST_MODIFIED).getDate().toInstant(); -// return FileTime.from(instant); -// } - return EPOCH; - } catch (RepositoryException e) { - throw new JcrFsException("Cannot get last modified time", e); - } - } - - @Override - public FileTime lastAccessTime() { - return lastModifiedTime(); - } - - @Override - public FileTime creationTime() { - try { - if (node.hasProperty(JCR_CREATED)) { - Instant instant = node.getProperty(JCR_CREATED).getDate().toInstant(); - return FileTime.from(instant); - } else if (node.hasProperty(JCR_LAST_MODIFIED)) { - Instant instant = node.getProperty(JCR_LAST_MODIFIED).getDate().toInstant(); - return FileTime.from(instant); - } -// if (node.isNodeType(NodeType.MIX_CREATED)) { -// Instant instant = node.getProperty(JCR_CREATED).getDate().toInstant(); -// return FileTime.from(instant); -// } - return EPOCH; - } catch (RepositoryException e) { - throw new JcrFsException("Cannot get creation time", e); - } - } - - @Override - public boolean isRegularFile() { - try { - return node.isNodeType(NodeType.NT_FILE); - } catch (RepositoryException e) { - throw new JcrFsException("Cannot check if regular file", e); - } - } - - @Override - public boolean isDirectory() { - try { - if (node.isNodeType(NodeType.NT_FOLDER)) - return true; - // all other non file nodes - return !(node.isNodeType(NodeType.NT_FILE) || node.isNodeType(NodeType.NT_LINKED_FILE)); - } catch (RepositoryException e) { - throw new JcrFsException("Cannot check if directory", e); - } - } - - @Override - public boolean isSymbolicLink() { - try { - return node.isNodeType(NodeType.NT_LINKED_FILE); - } catch (RepositoryException e) { - throw new JcrFsException("Cannot check if linked file", e); - } - } - - @Override - public boolean isOther() { - return !(isDirectory() || isRegularFile() || isSymbolicLink()); - } - - @Override - public long size() { - if (isRegularFile()) { - Binary binary = null; - try { - binary = node.getNode(Property.JCR_CONTENT).getProperty(Property.JCR_DATA).getBinary(); - return binary.getSize(); - } catch (RepositoryException e) { - throw new JcrFsException("Cannot check size", e); - } finally { - JcrUtils.closeQuietly(binary); - } - } - return -1; - } - - @Override - public Object fileKey() { - try { - return node.getIdentifier(); - } catch (RepositoryException e) { - throw new JcrFsException("Cannot get identifier", e); - } - } - - @Override - public Node getNode() { - return node; - } - -} diff --git a/org.argeo.jcr/src/org/argeo/jcr/fs/JcrFileSystem.java b/org.argeo.jcr/src/org/argeo/jcr/fs/JcrFileSystem.java deleted file mode 100644 index 3d538e8bd..000000000 --- a/org.argeo.jcr/src/org/argeo/jcr/fs/JcrFileSystem.java +++ /dev/null @@ -1,250 +0,0 @@ -package org.argeo.jcr.fs; - -import java.io.IOException; -import java.nio.file.FileStore; -import java.nio.file.FileSystem; -import java.nio.file.Path; -import java.nio.file.PathMatcher; -import java.nio.file.WatchService; -import java.nio.file.attribute.UserPrincipalLookupService; -import java.nio.file.spi.FileSystemProvider; -import java.util.ArrayList; -import java.util.HashSet; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.TreeMap; - -import javax.jcr.Credentials; -import javax.jcr.Node; -import javax.jcr.Repository; -import javax.jcr.RepositoryException; -import javax.jcr.Session; -import javax.jcr.nodetype.NodeType; - -import org.argeo.jcr.Jcr; -import org.argeo.jcr.JcrUtils; - -public class JcrFileSystem extends FileSystem { - private final JcrFileSystemProvider provider; - - private final Repository repository; - private Session session; - private WorkspaceFileStore baseFileStore; - - private Map mounts = new TreeMap<>(); - - private String userHomePath = null; - - @Deprecated - public JcrFileSystem(JcrFileSystemProvider provider, Session session) throws IOException { - super(); - this.provider = provider; - baseFileStore = new WorkspaceFileStore(null, session.getWorkspace()); - this.session = session; -// Node userHome = provider.getUserHome(session); -// if (userHome != null) -// try { -// userHomePath = userHome.getPath(); -// } catch (RepositoryException e) { -// throw new IOException("Cannot retrieve user home path", e); -// } - this.repository = null; - } - - public JcrFileSystem(JcrFileSystemProvider provider, Repository repository) throws IOException { - this(provider, repository, null); - } - - public JcrFileSystem(JcrFileSystemProvider provider, Repository repository, Credentials credentials) - throws IOException { - super(); - this.provider = provider; - this.repository = repository; - try { - this.session = credentials == null ? repository.login() : repository.login(credentials); - baseFileStore = new WorkspaceFileStore(null, session.getWorkspace()); - workspaces: for (String workspaceName : baseFileStore.getWorkspace().getAccessibleWorkspaceNames()) { - if (workspaceName.equals(baseFileStore.getWorkspace().getName())) - continue workspaces;// do not mount base - if (workspaceName.equals("security")) { - continue workspaces;// do not mount security workspace - // TODO make it configurable - } - Session mountSession = credentials == null ? repository.login(workspaceName) - : repository.login(credentials, workspaceName); - String mountPath = JcrPath.separator + workspaceName; - mounts.put(mountPath, new WorkspaceFileStore(mountPath, mountSession.getWorkspace())); - } - } catch (RepositoryException e) { - throw new IOException("Cannot initialise file system", e); - } - - Node userHome = provider.getUserHome(repository); - if (userHome != null) - try { - userHomePath = toFsPath(userHome); - } catch (RepositoryException e) { - throw new IOException("Cannot retrieve user home path", e); - } finally { - JcrUtils.logoutQuietly(Jcr.session(userHome)); - } - } - - public String toFsPath(Node node) throws RepositoryException { - return getFileStore(node).toFsPath(node); - } - - /** Whether this node should be skipped in directory listings */ - public boolean skipNode(Node node) throws RepositoryException { - if (node.isNodeType(NodeType.NT_HIERARCHY_NODE)) - return false; - return true; - } - - public String getUserHomePath() { - return userHomePath; - } - - public WorkspaceFileStore getFileStore(String path) { - WorkspaceFileStore res = baseFileStore; - for (String mountPath : mounts.keySet()) { - if (path.equals(mountPath)) - return mounts.get(mountPath); - if (path.startsWith(mountPath + JcrPath.separator)) { - res = mounts.get(mountPath); - // we keep the last one - } - } - assert res != null; - return res; - } - - public WorkspaceFileStore getFileStore(Node node) throws RepositoryException { - String workspaceName = node.getSession().getWorkspace().getName(); - if (workspaceName.equals(baseFileStore.getWorkspace().getName())) - return baseFileStore; - for (String mountPath : mounts.keySet()) { - WorkspaceFileStore fileStore = mounts.get(mountPath); - if (workspaceName.equals(fileStore.getWorkspace().getName())) - return fileStore; - } - throw new IllegalStateException("No workspace mount found for " + node + " in workspace " + workspaceName); - } - - public Iterator listDirectMounts(Path base) { - String baseStr = base.toString(); - Set res = new HashSet<>(); - mounts: for (String mountPath : mounts.keySet()) { - if (mountPath.equals(baseStr)) - continue mounts; - if (mountPath.startsWith(baseStr)) { - JcrPath path = new JcrPath(this, mountPath); - Path relPath = base.relativize(path); - if (relPath.getNameCount() == 1) - res.add(path); - } - } - return res.iterator(); - } - - public WorkspaceFileStore getBaseFileStore() { - return baseFileStore; - } - - @Override - public FileSystemProvider provider() { - return provider; - } - - @Override - public void close() throws IOException { - JcrUtils.logoutQuietly(session); - for (String mountPath : mounts.keySet()) { - WorkspaceFileStore fileStore = mounts.get(mountPath); - try { - fileStore.close(); - } catch (Exception e) { - e.printStackTrace(); - } - } - } - - @Override - public boolean isOpen() { - return session.isLive(); - } - - @Override - public boolean isReadOnly() { - return false; - } - - @Override - public String getSeparator() { - return JcrPath.separator; - } - - @Override - public Iterable getRootDirectories() { - Set single = new HashSet<>(); - single.add(new JcrPath(this, JcrPath.separator)); - return single; - } - - @Override - public Iterable getFileStores() { - List stores = new ArrayList<>(); - stores.add(baseFileStore); - stores.addAll(mounts.values()); - return stores; - } - - @Override - public Set supportedFileAttributeViews() { - try { - String[] prefixes = session.getNamespacePrefixes(); - Set res = new HashSet<>(); - for (String prefix : prefixes) - res.add(prefix); - res.add("basic"); - return res; - } catch (RepositoryException e) { - throw new JcrFsException("Cannot get supported file attributes views", e); - } - } - - @Override - public Path getPath(String first, String... more) { - StringBuilder sb = new StringBuilder(first); - // TODO Make it more robust - for (String part : more) - sb.append('/').append(part); - return new JcrPath(this, sb.toString()); - } - - @Override - public PathMatcher getPathMatcher(String syntaxAndPattern) { - throw new UnsupportedOperationException(); - } - - @Override - public UserPrincipalLookupService getUserPrincipalLookupService() { - throw new UnsupportedOperationException(); - } - - @Override - public WatchService newWatchService() throws IOException { - throw new UnsupportedOperationException(); - } - -// public Session getSession() { -// return session; -// } - - public Repository getRepository() { - return repository; - } - -} diff --git a/org.argeo.jcr/src/org/argeo/jcr/fs/JcrFileSystemProvider.java b/org.argeo.jcr/src/org/argeo/jcr/fs/JcrFileSystemProvider.java deleted file mode 100644 index 74d9a198e..000000000 --- a/org.argeo.jcr/src/org/argeo/jcr/fs/JcrFileSystemProvider.java +++ /dev/null @@ -1,337 +0,0 @@ -package org.argeo.jcr.fs; - -import java.io.IOException; -import java.nio.channels.SeekableByteChannel; -import java.nio.file.AccessMode; -import java.nio.file.CopyOption; -import java.nio.file.DirectoryNotEmptyException; -import java.nio.file.DirectoryStream; -import java.nio.file.DirectoryStream.Filter; -import java.nio.file.FileStore; -import java.nio.file.LinkOption; -import java.nio.file.NoSuchFileException; -import java.nio.file.OpenOption; -import java.nio.file.Path; -import java.nio.file.attribute.BasicFileAttributes; -import java.nio.file.attribute.FileAttribute; -import java.nio.file.attribute.FileAttributeView; -import java.nio.file.spi.FileSystemProvider; -import java.util.Calendar; -import java.util.HashMap; -import java.util.Map; -import java.util.Set; - -import javax.jcr.Node; -import javax.jcr.Property; -import javax.jcr.PropertyIterator; -import javax.jcr.PropertyType; -import javax.jcr.Repository; -import javax.jcr.RepositoryException; -import javax.jcr.Session; -import javax.jcr.nodetype.NodeType; -import javax.jcr.nodetype.PropertyDefinition; - -import org.argeo.jcr.JcrUtils; - -/** Operations on a {@link JcrFileSystem}. */ -public abstract class JcrFileSystemProvider extends FileSystemProvider { - - @Override - public SeekableByteChannel newByteChannel(Path path, Set options, FileAttribute... attrs) - throws IOException { - Node node = toNode(path); - try { - if (node == null) { - Node parent = toNode(path.getParent()); - if (parent == null) - throw new IOException("No parent directory for " + path); - if (parent.getPrimaryNodeType().isNodeType(NodeType.NT_FILE) - || parent.getPrimaryNodeType().isNodeType(NodeType.NT_LINKED_FILE)) - throw new IOException(path + " parent is a file"); - - String fileName = path.getFileName().toString(); - fileName = Text.escapeIllegalJcrChars(fileName); - node = parent.addNode(fileName, NodeType.NT_FILE); - node.addMixin(NodeType.MIX_CREATED); -// node.addMixin(NodeType.MIX_LAST_MODIFIED); - } - if (!node.isNodeType(NodeType.NT_FILE)) - throw new UnsupportedOperationException(node + " must be a file"); - return new BinaryChannel(node, path); - } catch (RepositoryException e) { - discardChanges(node); - throw new IOException("Cannot read file", e); - } - } - - @Override - public DirectoryStream newDirectoryStream(Path dir, Filter filter) throws IOException { - try { - Node base = toNode(dir); - if (base == null) - throw new IOException(dir + " is not a JCR node"); - JcrFileSystem fileSystem = (JcrFileSystem) dir.getFileSystem(); - return new NodeDirectoryStream(fileSystem, base.getNodes(), fileSystem.listDirectMounts(dir), filter); - } catch (RepositoryException e) { - throw new IOException("Cannot list directory", e); - } - } - - @Override - public void createDirectory(Path dir, FileAttribute... attrs) throws IOException { - Node node = toNode(dir); - try { - if (node == null) { - Node parent = toNode(dir.getParent()); - if (parent == null) - throw new IOException("Parent of " + dir + " does not exist"); - Session session = parent.getSession(); - synchronized (session) { - if (parent.getPrimaryNodeType().isNodeType(NodeType.NT_FILE) - || parent.getPrimaryNodeType().isNodeType(NodeType.NT_LINKED_FILE)) - throw new IOException(dir + " parent is a file"); - String fileName = dir.getFileName().toString(); - fileName = Text.escapeIllegalJcrChars(fileName); - node = parent.addNode(fileName, NodeType.NT_FOLDER); - node.addMixin(NodeType.MIX_CREATED); - node.addMixin(NodeType.MIX_LAST_MODIFIED); - save(session); - } - } else { - // if (!node.getPrimaryNodeType().isNodeType(NodeType.NT_FOLDER)) - // throw new FileExistsException(dir + " exists and is not a directory"); - } - } catch (RepositoryException e) { - discardChanges(node); - throw new IOException("Cannot create directory " + dir, e); - } - } - - @Override - public void delete(Path path) throws IOException { - Node node = toNode(path); - try { - if (node == null) - throw new NoSuchFileException(path + " does not exist"); - Session session = node.getSession(); - synchronized (session) { - session.refresh(false); - if (node.getPrimaryNodeType().isNodeType(NodeType.NT_FILE)) - node.remove(); - else if (node.getPrimaryNodeType().isNodeType(NodeType.NT_FOLDER)) { - if (node.hasNodes())// TODO check only files - throw new DirectoryNotEmptyException(path.toString()); - node.remove(); - } - save(session); - } - } catch (RepositoryException e) { - discardChanges(node); - throw new IOException("Cannot delete " + path, e); - } - - } - - @Override - public void copy(Path source, Path target, CopyOption... options) throws IOException { - Node sourceNode = toNode(source); - Node targetNode = toNode(target); - try { - Session targetSession = targetNode.getSession(); - synchronized (targetSession) { - JcrUtils.copy(sourceNode, targetNode); - save(targetSession); - } - } catch (RepositoryException e) { - discardChanges(sourceNode); - discardChanges(targetNode); - throw new IOException("Cannot copy from " + source + " to " + target, e); - } - } - - @Override - public void move(Path source, Path target, CopyOption... options) throws IOException { - JcrFileSystem sourceFileSystem = (JcrFileSystem) source.getFileSystem(); - WorkspaceFileStore sourceStore = sourceFileSystem.getFileStore(source.toString()); - WorkspaceFileStore targetStore = sourceFileSystem.getFileStore(target.toString()); - try { - if (sourceStore.equals(targetStore)) { - sourceStore.getWorkspace().move(sourceStore.toJcrPath(source.toString()), - targetStore.toJcrPath(target.toString())); - } else { - // TODO implement it - throw new UnsupportedOperationException("Can only move paths within the same workspace."); - } - } catch (RepositoryException e) { - throw new IOException("Cannot move from " + source + " to " + target, e); - } - -// Node sourceNode = toNode(source); -// try { -// Session session = sourceNode.getSession(); -// synchronized (session) { -// session.move(sourceNode.getPath(), target.toString()); -// save(session); -// } -// } catch (RepositoryException e) { -// discardChanges(sourceNode); -// throw new IOException("Cannot move from " + source + " to " + target, e); -// } - } - - @Override - public boolean isSameFile(Path path, Path path2) throws IOException { - if (path.getFileSystem() != path2.getFileSystem()) - return false; - boolean equ = path.equals(path2); - if (equ) - return true; - else { - try { - Node node = toNode(path); - Node node2 = toNode(path2); - return node.isSame(node2); - } catch (RepositoryException e) { - throw new IOException("Cannot check whether " + path + " and " + path2 + " are same", e); - } - } - - } - - @Override - public boolean isHidden(Path path) throws IOException { - return path.getFileName().toString().charAt(0) == '.'; - } - - @Override - public FileStore getFileStore(Path path) throws IOException { - JcrFileSystem fileSystem = (JcrFileSystem) path.getFileSystem(); - return fileSystem.getFileStore(path.toString()); - } - - @Override - public void checkAccess(Path path, AccessMode... modes) throws IOException { - Node node = toNode(path); - if (node == null) - throw new NoSuchFileException(path + " does not exist"); - // TODO check access via JCR api - } - - @Override - public V getFileAttributeView(Path path, Class type, LinkOption... options) { - throw new UnsupportedOperationException(); - } - - @SuppressWarnings("unchecked") - @Override - public A readAttributes(Path path, Class type, LinkOption... options) - throws IOException { - // TODO check if assignable - Node node = toNode(path); - if (node == null) { - throw new IOException("JCR node not found for " + path); - } - return (A) new JcrBasicfileAttributes(node); - } - - @Override - public Map readAttributes(Path path, String attributes, LinkOption... options) throws IOException { - try { - Node node = toNode(path); - String pattern = attributes.replace(',', '|'); - Map res = new HashMap(); - PropertyIterator it = node.getProperties(pattern); - props: while (it.hasNext()) { - Property prop = it.nextProperty(); - PropertyDefinition pd = prop.getDefinition(); - if (pd.isMultiple()) - continue props; - int requiredType = pd.getRequiredType(); - switch (requiredType) { - case PropertyType.LONG: - res.put(prop.getName(), prop.getLong()); - break; - case PropertyType.DOUBLE: - res.put(prop.getName(), prop.getDouble()); - break; - case PropertyType.BOOLEAN: - res.put(prop.getName(), prop.getBoolean()); - break; - case PropertyType.DATE: - res.put(prop.getName(), prop.getDate()); - break; - case PropertyType.BINARY: - byte[] arr = JcrUtils.getBinaryAsBytes(prop); - res.put(prop.getName(), arr); - break; - default: - res.put(prop.getName(), prop.getString()); - } - } - return res; - } catch (RepositoryException e) { - throw new IOException("Cannot read attributes of " + path, e); - } - } - - @Override - public void setAttribute(Path path, String attribute, Object value, LinkOption... options) throws IOException { - Node node = toNode(path); - try { - Session session = node.getSession(); - synchronized (session) { - if (value instanceof byte[]) { - JcrUtils.setBinaryAsBytes(node, attribute, (byte[]) value); - } else if (value instanceof Calendar) { - node.setProperty(attribute, (Calendar) value); - } else { - node.setProperty(attribute, value.toString()); - } - save(session); - } - } catch (RepositoryException e) { - discardChanges(node); - throw new IOException("Cannot set attribute " + attribute + " on " + path, e); - } - } - - protected Node toNode(Path path) { - try { - return ((JcrPath) path).getNode(); - } catch (RepositoryException e) { - throw new JcrFsException("Cannot convert path " + path + " to JCR Node", e); - } - } - - /** Discard changes in the underlying session */ - protected void discardChanges(Node node) { - if (node == null) - return; - try { - // discard changes - node.getSession().refresh(false); - } catch (RepositoryException e) { - e.printStackTrace(); - // TODO log out session? - // TODO use Commons logging? - } - } - - /** Make sure save is robust. */ - protected void save(Session session) throws RepositoryException { - session.refresh(true); - session.save(); - session.notifyAll(); - } - - /** - * To be overriden in order to support the ~ path, with an implementation - * specific concept of user home. - * - * @return null by default - */ - public Node getUserHome(Repository session) { - return null; - } - -} diff --git a/org.argeo.jcr/src/org/argeo/jcr/fs/JcrFsException.java b/org.argeo.jcr/src/org/argeo/jcr/fs/JcrFsException.java deleted file mode 100644 index f214fdc44..000000000 --- a/org.argeo.jcr/src/org/argeo/jcr/fs/JcrFsException.java +++ /dev/null @@ -1,15 +0,0 @@ -package org.argeo.jcr.fs; - - -/** Exception related to the JCR FS */ -public class JcrFsException extends RuntimeException { - private static final long serialVersionUID = -7973896038244922980L; - - public JcrFsException(String message, Throwable e) { - super(message, e); - } - - public JcrFsException(String message) { - super(message); - } -} diff --git a/org.argeo.jcr/src/org/argeo/jcr/fs/JcrPath.java b/org.argeo.jcr/src/org/argeo/jcr/fs/JcrPath.java deleted file mode 100644 index 1a4d74706..000000000 --- a/org.argeo.jcr/src/org/argeo/jcr/fs/JcrPath.java +++ /dev/null @@ -1,393 +0,0 @@ -package org.argeo.jcr.fs; - -import java.io.File; -import java.io.IOException; -import java.net.URI; -import java.net.URISyntaxException; -import java.nio.file.FileSystem; -import java.nio.file.LinkOption; -import java.nio.file.Path; -import java.nio.file.WatchEvent.Kind; -import java.nio.file.WatchEvent.Modifier; -import java.nio.file.WatchKey; -import java.nio.file.WatchService; -import java.util.Arrays; -import java.util.Iterator; -import java.util.NoSuchElementException; - -import javax.jcr.Node; -import javax.jcr.RepositoryException; - -/** A {@link Path} which contains a reference to a JCR {@link Node}. */ -public class JcrPath implements Path { - final static String separator = "/"; - final static char separatorChar = '/'; - - private final JcrFileSystem fs; - /** null for non absolute paths */ - private final WorkspaceFileStore fileStore; - private final String[] path;// null means root - private final boolean absolute; - - // optim - private final int hashCode; - - public JcrPath(JcrFileSystem filesSystem, String path) { - this.fs = filesSystem; - if (path == null) - throw new JcrFsException("Path cannot be null"); - if (path.equals(separator)) {// root - this.path = null; - this.absolute = true; - this.hashCode = 0; - this.fileStore = fs.getBaseFileStore(); - return; - } else if (path.equals("")) {// empty path - this.path = new String[] { "" }; - this.absolute = false; - this.fileStore = null; - this.hashCode = "".hashCode(); - return; - } - - if (path.equals("~")) {// home - path = filesSystem.getUserHomePath(); - if (path == null) - throw new JcrFsException("No home directory available"); - } - - this.absolute = path.charAt(0) == separatorChar ? true : false; - - this.fileStore = absolute ? fs.getFileStore(path) : null; - - String trimmedPath = path.substring(absolute ? 1 : 0, - path.charAt(path.length() - 1) == separatorChar ? path.length() - 1 : path.length()); - this.path = trimmedPath.split(separator); - for (int i = 0; i < this.path.length; i++) { - this.path[i] = Text.unescapeIllegalJcrChars(this.path[i]); - } - this.hashCode = this.path[this.path.length - 1].hashCode(); - assert !(absolute && fileStore == null); - } - - public JcrPath(JcrFileSystem filesSystem, Node node) throws RepositoryException { - this(filesSystem, filesSystem.getFileStore(node).toFsPath(node)); - } - - /** Internal optimisation */ - private JcrPath(JcrFileSystem filesSystem, WorkspaceFileStore fileStore, String[] path, boolean absolute) { - this.fs = filesSystem; - this.path = path; - this.absolute = path == null ? true : absolute; - if (this.absolute && fileStore == null) - throw new IllegalArgumentException("Absolute path requires a file store"); - if (!this.absolute && fileStore != null) - throw new IllegalArgumentException("A file store should not be provided for a relative path"); - this.fileStore = fileStore; - this.hashCode = path == null ? 0 : path[path.length - 1].hashCode(); - assert !(absolute && fileStore == null); - } - - @Override - public FileSystem getFileSystem() { - return fs; - } - - @Override - public boolean isAbsolute() { - return absolute; - } - - @Override - public Path getRoot() { - if (path == null) - return this; - return new JcrPath(fs, separator); - } - - @Override - public String toString() { - return toFsPath(path); - } - - private String toFsPath(String[] path) { - if (path == null) - return "/"; - StringBuilder sb = new StringBuilder(); - if (isAbsolute()) - sb.append('/'); - for (int i = 0; i < path.length; i++) { - if (i != 0) - sb.append('/'); - sb.append(path[i]); - } - return sb.toString(); - } - -// @Deprecated -// private String toJcrPath() { -// return toJcrPath(path); -// } -// -// @Deprecated -// private String toJcrPath(String[] path) { -// if (path == null) -// return "/"; -// StringBuilder sb = new StringBuilder(); -// if (isAbsolute()) -// sb.append('/'); -// for (int i = 0; i < path.length; i++) { -// if (i != 0) -// sb.append('/'); -// sb.append(Text.escapeIllegalJcrChars(path[i])); -// } -// return sb.toString(); -// } - - @Override - public Path getFileName() { - if (path == null) - return null; - return new JcrPath(fs, path[path.length - 1]); - } - - @Override - public Path getParent() { - if (path == null) - return null; - if (path.length == 1)// root - return new JcrPath(fs, separator); - String[] parentPath = Arrays.copyOfRange(path, 0, path.length - 1); - if (!absolute) - return new JcrPath(fs, null, parentPath, absolute); - else - return new JcrPath(fs, toFsPath(parentPath)); - } - - @Override - public int getNameCount() { - if (path == null) - return 0; - return path.length; - } - - @Override - public Path getName(int index) { - if (path == null) - return null; - return new JcrPath(fs, path[index]); - } - - @Override - public Path subpath(int beginIndex, int endIndex) { - if (path == null) - return null; - String[] parentPath = Arrays.copyOfRange(path, beginIndex, endIndex); - return new JcrPath(fs, null, parentPath, false); - } - - @Override - public boolean startsWith(Path other) { - return toString().startsWith(other.toString()); - } - - @Override - public boolean startsWith(String other) { - return toString().startsWith(other); - } - - @Override - public boolean endsWith(Path other) { - return toString().endsWith(other.toString()); - } - - @Override - public boolean endsWith(String other) { - return toString().endsWith(other); - } - - @Override - public Path normalize() { - // always normalized - return this; - } - - @Override - public Path resolve(Path other) { - JcrPath otherPath = (JcrPath) other; - if (otherPath.isAbsolute()) - return other; - String[] newPath; - if (path == null) { - newPath = new String[otherPath.path.length]; - System.arraycopy(otherPath.path, 0, newPath, 0, otherPath.path.length); - } else { - newPath = new String[path.length + otherPath.path.length]; - System.arraycopy(path, 0, newPath, 0, path.length); - System.arraycopy(otherPath.path, 0, newPath, path.length, otherPath.path.length); - } - if (!absolute) - return new JcrPath(fs, null, newPath, absolute); - else { - return new JcrPath(fs, toFsPath(newPath)); - } - } - - @Override - public final Path resolve(String other) { - return resolve(getFileSystem().getPath(other)); - } - - @Override - public final Path resolveSibling(Path other) { - if (other == null) - throw new NullPointerException(); - Path parent = getParent(); - return (parent == null) ? other : parent.resolve(other); - } - - @Override - public final Path resolveSibling(String other) { - return resolveSibling(getFileSystem().getPath(other)); - } - - @Override - public final Iterator iterator() { - return new Iterator() { - private int i = 0; - - @Override - public boolean hasNext() { - return (i < getNameCount()); - } - - @Override - public Path next() { - if (i < getNameCount()) { - Path result = getName(i); - i++; - return result; - } else { - throw new NoSuchElementException(); - } - } - - @Override - public void remove() { - throw new UnsupportedOperationException(); - } - }; - } - - @Override - public Path relativize(Path other) { - if (equals(other)) - return new JcrPath(fs, ""); - if (other.startsWith(this)) { - String p1 = toString(); - String p2 = other.toString(); - String relative = p2.substring(p1.length(), p2.length()); - if (relative.charAt(0) == '/') - relative = relative.substring(1); - return new JcrPath(fs, relative); - } - throw new IllegalArgumentException(other + " cannot be relativized against " + this); - } - - @Override - public URI toUri() { - try { - return new URI(fs.provider().getScheme(), toString(), null); - } catch (URISyntaxException e) { - throw new JcrFsException("Cannot create URI for " + toString(), e); - } - } - - @Override - public Path toAbsolutePath() { - if (isAbsolute()) - return this; - return new JcrPath(fs, fileStore, path, true); - } - - @Override - public Path toRealPath(LinkOption... options) throws IOException { - return this; - } - - @Override - public File toFile() { - throw new UnsupportedOperationException(); - } - - @Override - public WatchKey register(WatchService watcher, Kind[] events, Modifier... modifiers) throws IOException { - // TODO Auto-generated method stub - return null; - } - - @Override - public WatchKey register(WatchService watcher, Kind... events) throws IOException { - // TODO Auto-generated method stub - return null; - } - - @Override - public int compareTo(Path other) { - return toString().compareTo(other.toString()); - } - - public Node getNode() throws RepositoryException { - if (!isAbsolute())// TODO default dir - throw new JcrFsException("Cannot get a JCR node from a relative path"); - assert fileStore != null; - return fileStore.toNode(path); -// String pathStr = toJcrPath(); -// Session session = fs.getSession(); -// // TODO synchronize on the session ? -// if (!session.itemExists(pathStr)) -// return null; -// return session.getNode(pathStr); - } - - @Override - public boolean equals(Object obj) { - if (!(obj instanceof JcrPath)) - return false; - JcrPath other = (JcrPath) obj; - - if (path == null) {// root - if (other.path == null)// root - return true; - else - return false; - } else { - if (other.path == null)// root - return false; - } - // non root - if (path.length != other.path.length) - return false; - for (int i = 0; i < path.length; i++) { - if (!path[i].equals(other.path[i])) - return false; - } - return true; - } - - @Override - public int hashCode() { - return hashCode; - } - - @Override - protected Object clone() throws CloneNotSupportedException { - return new JcrPath(fs, toString()); - } - - @Override - protected void finalize() throws Throwable { - Arrays.fill(path, null); - } - -} diff --git a/org.argeo.jcr/src/org/argeo/jcr/fs/NodeDirectoryStream.java b/org.argeo.jcr/src/org/argeo/jcr/fs/NodeDirectoryStream.java deleted file mode 100644 index eda07a548..000000000 --- a/org.argeo.jcr/src/org/argeo/jcr/fs/NodeDirectoryStream.java +++ /dev/null @@ -1,77 +0,0 @@ -package org.argeo.jcr.fs; - -import java.io.IOException; -import java.nio.file.DirectoryStream; -import java.nio.file.Path; -import java.util.Iterator; - -import javax.jcr.Node; -import javax.jcr.NodeIterator; - -public class NodeDirectoryStream implements DirectoryStream { - private final JcrFileSystem fs; - private final NodeIterator nodeIterator; - private final Iterator additionalPaths; - private final Filter filter; - - public NodeDirectoryStream(JcrFileSystem fs, NodeIterator nodeIterator, Iterator additionalPaths, - Filter filter) { - this.fs = fs; - this.nodeIterator = nodeIterator; - this.additionalPaths = additionalPaths; - this.filter = filter; - } - - @Override - public void close() throws IOException { - } - - @Override - public Iterator iterator() { - return new Iterator() { - private JcrPath next = null; - - @Override - public synchronized boolean hasNext() { - if (next != null) - return true; - nodes: while (nodeIterator.hasNext()) { - try { - Node node = nodeIterator.nextNode(); - String nodeName = node.getName(); - if (nodeName.startsWith("rep:") || nodeName.startsWith("jcr:")) - continue nodes; - if (fs.skipNode(node)) - continue nodes; - next = new JcrPath(fs, node); - if (filter != null) { - if (filter.accept(next)) - break nodes; - } else - break nodes; - } catch (Exception e) { - throw new JcrFsException("Could not get next path", e); - } - } - - if (next == null) { - if (additionalPaths.hasNext()) - next = additionalPaths.next(); - } - - return next != null; - } - - @Override - public synchronized Path next() { - if (!hasNext())// make sure has next has been called - return null; - JcrPath res = next; - next = null; - return res; - } - - }; - } - -} diff --git a/org.argeo.jcr/src/org/argeo/jcr/fs/NodeFileAttributes.java b/org.argeo.jcr/src/org/argeo/jcr/fs/NodeFileAttributes.java deleted file mode 100644 index 8054d52f8..000000000 --- a/org.argeo.jcr/src/org/argeo/jcr/fs/NodeFileAttributes.java +++ /dev/null @@ -1,9 +0,0 @@ -package org.argeo.jcr.fs; - -import java.nio.file.attribute.BasicFileAttributes; - -import javax.jcr.Node; - -public interface NodeFileAttributes extends BasicFileAttributes { - public Node getNode(); -} diff --git a/org.argeo.jcr/src/org/argeo/jcr/fs/Text.java b/org.argeo.jcr/src/org/argeo/jcr/fs/Text.java deleted file mode 100644 index 4643c8c9c..000000000 --- a/org.argeo.jcr/src/org/argeo/jcr/fs/Text.java +++ /dev/null @@ -1,877 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You 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. - */ -package org.argeo.jcr.fs; - -import java.io.ByteArrayOutputStream; -import java.io.UnsupportedEncodingException; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; -import java.util.ArrayList; -import java.util.BitSet; -import java.util.Properties; - -/** - * Hacked from org.apache.jackrabbit.util.Text in Jackrabbit JCR Commons - * This Class provides some text related utilities - */ -class Text { - - /** - * Hidden constructor. - */ - private Text() { - } - - /** - * used for the md5 - */ - public static final char[] hexTable = "0123456789abcdef".toCharArray(); - - /** - * Calculate an MD5 hash of the string given. - * - * @param data - * the data to encode - * @param enc - * the character encoding to use - * @return a hex encoded string of the md5 digested input - */ - public static String md5(String data, String enc) throws UnsupportedEncodingException { - try { - return digest("MD5", data.getBytes(enc)); - } catch (NoSuchAlgorithmException e) { - throw new InternalError("MD5 digest not available???"); - } - } - - /** - * Calculate an MD5 hash of the string given using 'utf-8' encoding. - * - * @param data - * the data to encode - * @return a hex encoded string of the md5 digested input - */ - public static String md5(String data) { - try { - return md5(data, "utf-8"); - } catch (UnsupportedEncodingException e) { - throw new InternalError("UTF8 digest not available???"); - } - } - - /** - * Digest the plain string using the given algorithm. - * - * @param algorithm - * The alogrithm for the digest. This algorithm must be supported - * by the MessageDigest class. - * @param data - * The plain text String to be digested. - * @param enc - * The character encoding to use - * @return The digested plain text String represented as Hex digits. - * @throws java.security.NoSuchAlgorithmException - * if the desired algorithm is not supported by the - * MessageDigest class. - * @throws java.io.UnsupportedEncodingException - * if the encoding is not supported - */ - public static String digest(String algorithm, String data, String enc) - throws NoSuchAlgorithmException, UnsupportedEncodingException { - - return digest(algorithm, data.getBytes(enc)); - } - - /** - * Digest the plain string using the given algorithm. - * - * @param algorithm - * The algorithm for the digest. This algorithm must be supported - * by the MessageDigest class. - * @param data - * the data to digest with the given algorithm - * @return The digested plain text String represented as Hex digits. - * @throws java.security.NoSuchAlgorithmException - * if the desired algorithm is not supported by the - * MessageDigest class. - */ - public static String digest(String algorithm, byte[] data) throws NoSuchAlgorithmException { - - MessageDigest md = MessageDigest.getInstance(algorithm); - byte[] digest = md.digest(data); - StringBuilder res = new StringBuilder(digest.length * 2); - for (byte b : digest) { - res.append(hexTable[(b >> 4) & 15]); - res.append(hexTable[b & 15]); - } - return res.toString(); - } - - /** - * returns an array of strings decomposed of the original string, split at - * every occurrence of 'ch'. if 2 'ch' follow each other with no - * intermediate characters, empty "" entries are avoided. - * - * @param str - * the string to decompose - * @param ch - * the character to use a split pattern - * @return an array of strings - */ - public static String[] explode(String str, int ch) { - return explode(str, ch, false); - } - - /** - * returns an array of strings decomposed of the original string, split at - * every occurrence of 'ch'. - * - * @param str - * the string to decompose - * @param ch - * the character to use a split pattern - * @param respectEmpty - * if true, empty elements are generated - * @return an array of strings - */ - public static String[] explode(String str, int ch, boolean respectEmpty) { - if (str == null || str.length() == 0) { - return new String[0]; - } - - ArrayList strings = new ArrayList(); - int pos; - int lastpos = 0; - - // add snipples - while ((pos = str.indexOf(ch, lastpos)) >= 0) { - if (pos - lastpos > 0 || respectEmpty) { - strings.add(str.substring(lastpos, pos)); - } - lastpos = pos + 1; - } - // add rest - if (lastpos < str.length()) { - strings.add(str.substring(lastpos)); - } else if (respectEmpty && lastpos == str.length()) { - strings.add(""); - } - - // return string array - return strings.toArray(new String[strings.size()]); - } - - /** - * Concatenates all strings in the string array using the specified - * delimiter. - * - * @param arr - * @param delim - * @return the concatenated string - */ - public static String implode(String[] arr, String delim) { - StringBuilder buf = new StringBuilder(); - for (int i = 0; i < arr.length; i++) { - if (i > 0) { - buf.append(delim); - } - buf.append(arr[i]); - } - return buf.toString(); - } - - /** - * Replaces all occurrences of oldString in text - * with newString. - * - * @param text - * @param oldString - * old substring to be replaced with newString - * @param newString - * new substring to replace occurrences of oldString - * @return a string - */ - public static String replace(String text, String oldString, String newString) { - if (text == null || oldString == null || newString == null) { - throw new IllegalArgumentException("null argument"); - } - int pos = text.indexOf(oldString); - if (pos == -1) { - return text; - } - int lastPos = 0; - StringBuilder sb = new StringBuilder(text.length()); - while (pos != -1) { - sb.append(text.substring(lastPos, pos)); - sb.append(newString); - lastPos = pos + oldString.length(); - pos = text.indexOf(oldString, lastPos); - } - if (lastPos < text.length()) { - sb.append(text.substring(lastPos)); - } - return sb.toString(); - } - - /** - * Replaces XML characters in the given string that might need escaping as - * XML text or attribute - * - * @param text - * text to be escaped - * @return a string - */ - public static String encodeIllegalXMLCharacters(String text) { - return encodeMarkupCharacters(text, false); - } - - /** - * Replaces HTML characters in the given string that might need escaping as - * HTML text or attribute - * - * @param text - * text to be escaped - * @return a string - */ - public static String encodeIllegalHTMLCharacters(String text) { - return encodeMarkupCharacters(text, true); - } - - private static String encodeMarkupCharacters(String text, boolean isHtml) { - if (text == null) { - throw new IllegalArgumentException("null argument"); - } - StringBuilder buf = null; - int length = text.length(); - int pos = 0; - for (int i = 0; i < length; i++) { - int ch = text.charAt(i); - switch (ch) { - case '<': - case '>': - case '&': - case '"': - case '\'': - if (buf == null) { - buf = new StringBuilder(); - } - if (i > 0) { - buf.append(text.substring(pos, i)); - } - pos = i + 1; - break; - default: - continue; - } - if (ch == '<') { - buf.append("<"); - } else if (ch == '>') { - buf.append(">"); - } else if (ch == '&') { - buf.append("&"); - } else if (ch == '"') { - buf.append("""); - } else if (ch == '\'') { - buf.append(isHtml ? "'" : "'"); - } - } - if (buf == null) { - return text; - } else { - if (pos < length) { - buf.append(text.substring(pos)); - } - return buf.toString(); - } - } - - /** - * The list of characters that are not encoded by the escape() - * and unescape() METHODS. They contains the characters as - * defined 'unreserved' in section 2.3 of the RFC 2396 'URI generic syntax': - *

- * - *

-	 * unreserved  = alphanum | mark
-	 * mark        = "-" | "_" | "." | "!" | "~" | "*" | "'" | "(" | ")"
-	 * 
- */ - public static BitSet URISave; - - /** - * Same as {@link #URISave} but also contains the '/' - */ - public static BitSet URISaveEx; - - static { - URISave = new BitSet(256); - int i; - for (i = 'a'; i <= 'z'; i++) { - URISave.set(i); - } - for (i = 'A'; i <= 'Z'; i++) { - URISave.set(i); - } - for (i = '0'; i <= '9'; i++) { - URISave.set(i); - } - URISave.set('-'); - URISave.set('_'); - URISave.set('.'); - URISave.set('!'); - URISave.set('~'); - URISave.set('*'); - URISave.set('\''); - URISave.set('('); - URISave.set(')'); - - URISaveEx = (BitSet) URISave.clone(); - URISaveEx.set('/'); - } - - /** - * Does an URL encoding of the string using the - * escape character. The characters that don't need encoding - * are those defined 'unreserved' in section 2.3 of the 'URI generic syntax' - * RFC 2396, but without the escape character. - * - * @param string - * the string to encode. - * @param escape - * the escape character. - * @return the escaped string - * @throws NullPointerException - * if string is null. - */ - public static String escape(String string, char escape) { - return escape(string, escape, false); - } - - /** - * Does an URL encoding of the string using the - * escape character. The characters that don't need encoding - * are those defined 'unreserved' in section 2.3 of the 'URI generic syntax' - * RFC 2396, but without the escape character. If isPath is - * true, additionally the slash '/' is ignored, too. - * - * @param string - * the string to encode. - * @param escape - * the escape character. - * @param isPath - * if true, the string is treated as path - * @return the escaped string - * @throws NullPointerException - * if string is null. - */ - public static String escape(String string, char escape, boolean isPath) { - try { - BitSet validChars = isPath ? URISaveEx : URISave; - byte[] bytes = string.getBytes("utf-8"); - StringBuilder out = new StringBuilder(bytes.length); - for (byte aByte : bytes) { - int c = aByte & 0xff; - if (validChars.get(c) && c != escape) { - out.append((char) c); - } else { - out.append(escape); - out.append(hexTable[(c >> 4) & 0x0f]); - out.append(hexTable[(c) & 0x0f]); - } - } - return out.toString(); - } catch (UnsupportedEncodingException e) { - throw new InternalError(e.toString()); - } - } - - /** - * Does a URL encoding of the string. The characters that don't - * need encoding are those defined 'unreserved' in section 2.3 of the 'URI - * generic syntax' RFC 2396. - * - * @param string - * the string to encode - * @return the escaped string - * @throws NullPointerException - * if string is null. - */ - public static String escape(String string) { - return escape(string, '%'); - } - - /** - * Does a URL encoding of the path. The characters that don't - * need encoding are those defined 'unreserved' in section 2.3 of the 'URI - * generic syntax' RFC 2396. In contrast to the {@link #escape(String)} - * method, not the entire path string is escaped, but every individual part - * (i.e. the slashes are not escaped). - * - * @param path - * the path to encode - * @return the escaped path - * @throws NullPointerException - * if path is null. - */ - public static String escapePath(String path) { - return escape(path, '%', true); - } - - /** - * Does a URL decoding of the string using the - * escape character. Please note that in opposite to the - * {@link java.net.URLDecoder} it does not transform the + into spaces. - * - * @param string - * the string to decode - * @param escape - * the escape character - * @return the decoded string - * @throws NullPointerException - * if string is null. - * @throws IllegalArgumentException - * if the 2 characters following the escape character do not - * represent a hex-number or if not enough characters follow an - * escape character - */ - public static String unescape(String string, char escape) { - try { - byte[] utf8 = string.getBytes("utf-8"); - - // Check whether escape occurs at invalid position - if ((utf8.length >= 1 && utf8[utf8.length - 1] == escape) - || (utf8.length >= 2 && utf8[utf8.length - 2] == escape)) { - throw new IllegalArgumentException("Premature end of escape sequence at end of input"); - } - - ByteArrayOutputStream out = new ByteArrayOutputStream(utf8.length); - for (int k = 0; k < utf8.length; k++) { - byte b = utf8[k]; - if (b == escape) { - out.write((decodeDigit(utf8[++k]) << 4) + decodeDigit(utf8[++k])); - } else { - out.write(b); - } - } - - return new String(out.toByteArray(), "utf-8"); - } catch (UnsupportedEncodingException e) { - throw new InternalError(e.toString()); - } - } - - /** - * Does a URL decoding of the string. Please note that in - * opposite to the {@link java.net.URLDecoder} it does not transform the + - * into spaces. - * - * @param string - * the string to decode - * @return the decoded string - * @throws NullPointerException - * if string is null. - * @throws ArrayIndexOutOfBoundsException - * if not enough character follow an escape character - * @throws IllegalArgumentException - * if the 2 characters following the escape character do not - * represent a hex-number. - */ - public static String unescape(String string) { - return unescape(string, '%'); - } - - /** - * Escapes all illegal JCR name characters of a string. The encoding is - * loosely modeled after URI encoding, but only encodes the characters it - * absolutely needs to in order to make the resulting string a valid JCR - * name. Use {@link #unescapeIllegalJcrChars(String)} for decoding. - *

- * QName EBNF:
- *

simplename ::= onecharsimplename | twocharsimplename | - * threeormorecharname onecharsimplename ::= (* Any Unicode character - * except: '.', '/', ':', '[', ']', '*', '|' or any whitespace character *) - * twocharsimplename ::= '.' onecharsimplename | onecharsimplename '.' | - * onecharsimplename onecharsimplename threeormorecharname ::= nonspace - * string nonspace string ::= char | string char char ::= nonspace | ' ' - * nonspace ::= (* Any Unicode character except: '/', ':', '[', ']', '*', - * '|' or any whitespace character *) - * - * @param name - * the name to escape - * @return the escaped name - */ - public static String escapeIllegalJcrChars(String name) { - return escapeIllegalChars(name, "%/:[]*|\t\r\n"); - } - - /** - * Escapes all illegal JCR 1.0 name characters of a string. Use - * {@link #unescapeIllegalJcrChars(String)} for decoding. - *

- * QName EBNF:
- *

simplename ::= onecharsimplename | twocharsimplename | - * threeormorecharname onecharsimplename ::= (* Any Unicode character - * except: '.', '/', ':', '[', ']', '*', ''', '"', '|' or any whitespace - * character *) twocharsimplename ::= '.' onecharsimplename | - * onecharsimplename '.' | onecharsimplename onecharsimplename - * threeormorecharname ::= nonspace string nonspace string ::= char | string - * char char ::= nonspace | ' ' nonspace ::= (* Any Unicode character - * except: '/', ':', '[', ']', '*', ''', '"', '|' or any whitespace - * character *) - * - * @since Apache Jackrabbit 2.3.2 and 2.2.10 - * @see
JCR-3128 - * @param name - * the name to escape - * @return the escaped name - */ - public static String escapeIllegalJcr10Chars(String name) { - return escapeIllegalChars(name, "%/:[]*'\"|\t\r\n"); - } - - private static String escapeIllegalChars(String name, String illegal) { - StringBuilder buffer = new StringBuilder(name.length() * 2); - for (int i = 0; i < name.length(); i++) { - char ch = name.charAt(i); - if (illegal.indexOf(ch) != -1 || (ch == '.' && name.length() < 3) - || (ch == ' ' && (i == 0 || i == name.length() - 1))) { - buffer.append('%'); - buffer.append(Character.toUpperCase(Character.forDigit(ch / 16, 16))); - buffer.append(Character.toUpperCase(Character.forDigit(ch % 16, 16))); - } else { - buffer.append(ch); - } - } - return buffer.toString(); - } - - /** - * Escapes illegal XPath search characters at the end of a string. - *

- * Example:
- * A search string like 'test?' will run into a ParseException documented in - * http://issues.apache.org/jira/browse/JCR-1248 - * - * @param s - * the string to encode - * @return the escaped string - */ - public static String escapeIllegalXpathSearchChars(String s) { - StringBuilder sb = new StringBuilder(); - sb.append(s.substring(0, (s.length() - 1))); - char c = s.charAt(s.length() - 1); - // NOTE: keep this in sync with _ESCAPED_CHAR below! - if (c == '!' || c == '(' || c == ':' || c == '^' || c == '[' || c == ']' || c == '{' || c == '}' || c == '?') { - sb.append('\\'); - } - sb.append(c); - return sb.toString(); - } - - /** - * Unescapes previously escaped jcr chars. - *

- * Please note, that this does not exactly the same as the url related - * {@link #unescape(String)}, since it handles the byte-encoding - * differently. - * - * @param name - * the name to unescape - * @return the unescaped name - */ - public static String unescapeIllegalJcrChars(String name) { - StringBuilder buffer = new StringBuilder(name.length()); - int i = name.indexOf('%'); - while (i > -1 && i + 2 < name.length()) { - buffer.append(name.toCharArray(), 0, i); - int a = Character.digit(name.charAt(i + 1), 16); - int b = Character.digit(name.charAt(i + 2), 16); - if (a > -1 && b > -1) { - buffer.append((char) (a * 16 + b)); - name = name.substring(i + 3); - } else { - buffer.append('%'); - name = name.substring(i + 1); - } - i = name.indexOf('%'); - } - buffer.append(name); - return buffer.toString(); - } - - /** - * Returns the name part of the path. If the given path is already a name - * (i.e. contains no slashes) it is returned. - * - * @param path - * the path - * @return the name part or null if path is - * null. - */ - public static String getName(String path) { - return getName(path, '/'); - } - - /** - * Returns the name part of the path, delimited by the given - * delim. If the given path is already a name (i.e. contains no - * delim characters) it is returned. - * - * @param path - * the path - * @param delim - * the delimiter - * @return the name part or null if path is - * null. - */ - public static String getName(String path, char delim) { - return path == null ? null : path.substring(path.lastIndexOf(delim) + 1); - } - - /** - * Same as {@link #getName(String)} but adding the possibility to pass paths - * that end with a trailing '/' - * - * @see #getName(String) - */ - public static String getName(String path, boolean ignoreTrailingSlash) { - if (ignoreTrailingSlash && path != null && path.endsWith("/") && path.length() > 1) { - path = path.substring(0, path.length() - 1); - } - return getName(path); - } - - /** - * Returns the namespace prefix of the given qname. If the - * prefix is missing, an empty string is returned. Please note, that this - * method does not validate the name or prefix. - *

- * the qname has the format: qname := [prefix ':'] local; - * - * @param qname - * a qualified name - * @return the prefix of the name or "". - * - * @see #getLocalName(String) - * - * @throws NullPointerException - * if qname is null - */ - public static String getNamespacePrefix(String qname) { - int pos = qname.indexOf(':'); - return pos >= 0 ? qname.substring(0, pos) : ""; - } - - /** - * Returns the local name of the given qname. Please note, that - * this method does not validate the name. - *

- * the qname has the format: qname := [prefix ':'] local; - * - * @param qname - * a qualified name - * @return the localname - * - * @see #getNamespacePrefix(String) - * - * @throws NullPointerException - * if qname is null - */ - public static String getLocalName(String qname) { - int pos = qname.indexOf(':'); - return pos >= 0 ? qname.substring(pos + 1) : qname; - } - - /** - * Determines, if two paths denote hierarchical siblins. - * - * @param p1 - * first path - * @param p2 - * second path - * @return true if on same level, false otherwise - */ - public static boolean isSibling(String p1, String p2) { - int pos1 = p1.lastIndexOf('/'); - int pos2 = p2.lastIndexOf('/'); - return (pos1 == pos2 && pos1 >= 0 && p1.regionMatches(0, p2, 0, pos1)); - } - - /** - * Determines if the descendant path is hierarchical a - * descendant of path. - * - * @param path - * the current path - * @param descendant - * the potential descendant - * @return true if the descendant is a descendant; - * false otherwise. - */ - public static boolean isDescendant(String path, String descendant) { - String pattern = path.endsWith("/") ? path : path + "/"; - return !pattern.equals(descendant) && descendant.startsWith(pattern); - } - - /** - * Determines if the descendant path is hierarchical a - * descendant of path or equal to it. - * - * @param path - * the path to check - * @param descendant - * the potential descendant - * @return true if the descendant is a descendant - * or equal; false otherwise. - */ - public static boolean isDescendantOrEqual(String path, String descendant) { - if (path.equals(descendant)) { - return true; - } else { - String pattern = path.endsWith("/") ? path : path + "/"; - return descendant.startsWith(pattern); - } - } - - /** - * Returns the nth relative parent of the path, where n=level. - *

- * Example:
- * - * Text.getRelativeParent("/foo/bar/test", 1) == "/foo/bar" - * - * - * @param path - * the path of the page - * @param level - * the level of the parent - */ - public static String getRelativeParent(String path, int level) { - int idx = path.length(); - while (level > 0) { - idx = path.lastIndexOf('/', idx - 1); - if (idx < 0) { - return ""; - } - level--; - } - return (idx == 0) ? "/" : path.substring(0, idx); - } - - /** - * Same as {@link #getRelativeParent(String, int)} but adding the - * possibility to pass paths that end with a trailing '/' - * - * @see #getRelativeParent(String, int) - */ - public static String getRelativeParent(String path, int level, boolean ignoreTrailingSlash) { - if (ignoreTrailingSlash && path.endsWith("/") && path.length() > 1) { - path = path.substring(0, path.length() - 1); - } - return getRelativeParent(path, level); - } - - /** - * Returns the nth absolute parent of the path, where n=level. - *

- * Example:
- * - * Text.getAbsoluteParent("/foo/bar/test", 1) == "/foo/bar" - * - * - * @param path - * the path of the page - * @param level - * the level of the parent - */ - public static String getAbsoluteParent(String path, int level) { - int idx = 0; - int len = path.length(); - while (level >= 0 && idx < len) { - idx = path.indexOf('/', idx + 1); - if (idx < 0) { - idx = len; - } - level--; - } - return level >= 0 ? "" : path.substring(0, idx); - } - - /** - * Performs variable replacement on the given string value. Each - * ${...} sequence within the given value is replaced with the - * value of the named parser variable. If a variable is not found in the - * properties an IllegalArgumentException is thrown unless - * ignoreMissing is true. In the later case, the - * missing variable is replaced by the empty string. - * - * @param value - * the original value - * @param ignoreMissing - * if true, missing variables are replaced by the - * empty string. - * @return value after variable replacements - * @throws IllegalArgumentException - * if the replacement of a referenced variable is not found - */ - public static String replaceVariables(Properties variables, String value, boolean ignoreMissing) - throws IllegalArgumentException { - StringBuilder result = new StringBuilder(); - - // Value: - // +--+-+--------+-+-----------------+ - // | |p|--> |q|--> | - // +--+-+--------+-+-----------------+ - int p = 0, q = value.indexOf("${"); // Find first ${ - while (q != -1) { - result.append(value.substring(p, q)); // Text before ${ - p = q; - q = value.indexOf("}", q + 2); // Find } - if (q != -1) { - String variable = value.substring(p + 2, q); - String replacement = variables.getProperty(variable); - if (replacement == null) { - if (ignoreMissing) { - replacement = ""; - } else { - throw new IllegalArgumentException("Replacement not found for ${" + variable + "}."); - } - } - result.append(replacement); - p = q + 1; - q = value.indexOf("${", p); // Find next ${ - } - } - result.append(value.substring(p, value.length())); // Trailing text - - return result.toString(); - } - - private static byte decodeDigit(byte b) { - if (b >= 0x30 && b <= 0x39) { - return (byte) (b - 0x30); - } else if (b >= 0x41 && b <= 0x46) { - return (byte) (b - 0x37); - } else if (b >= 0x61 && b <= 0x66) { - return (byte) (b - 0x57); - } else { - throw new IllegalArgumentException("Escape sequence is not hexadecimal: " + (char) b); - } - } - -} diff --git a/org.argeo.jcr/src/org/argeo/jcr/fs/WorkspaceFileStore.java b/org.argeo.jcr/src/org/argeo/jcr/fs/WorkspaceFileStore.java deleted file mode 100644 index 6d9d05c2a..000000000 --- a/org.argeo.jcr/src/org/argeo/jcr/fs/WorkspaceFileStore.java +++ /dev/null @@ -1,191 +0,0 @@ -package org.argeo.jcr.fs; - -import java.io.IOException; -import java.nio.file.FileStore; -import java.nio.file.attribute.FileAttributeView; -import java.nio.file.attribute.FileStoreAttributeView; -import java.util.Arrays; - -import javax.jcr.Node; -import javax.jcr.RepositoryException; -import javax.jcr.Session; -import javax.jcr.Workspace; - -import org.argeo.jcr.JcrUtils; - -/** A {@link FileStore} implementation based on JCR {@link Workspace}. */ -public class WorkspaceFileStore extends FileStore { - private final String mountPath; - private final Workspace workspace; - private final String workspaceName; - private final int mountDepth; - - public WorkspaceFileStore(String mountPath, Workspace workspace) { - if ("/".equals(mountPath) || "".equals(mountPath)) - throw new IllegalArgumentException( - "Mount path '" + mountPath + "' is unsupported, use null for the base file store"); - if (mountPath != null && !mountPath.startsWith(JcrPath.separator)) - throw new IllegalArgumentException("Mount path '" + mountPath + "' cannot end with /"); - if (mountPath != null && mountPath.endsWith(JcrPath.separator)) - throw new IllegalArgumentException("Mount path '" + mountPath + "' cannot end with /"); - this.mountPath = mountPath; - if (mountPath == null) - mountDepth = 0; - else { - mountDepth = mountPath.split(JcrPath.separator).length - 1; - } - this.workspace = workspace; - this.workspaceName = workspace.getName(); - } - - public void close() { - JcrUtils.logoutQuietly(workspace.getSession()); - } - - @Override - public String name() { - return workspace.getName(); - } - - @Override - public String type() { - return "workspace"; - } - - @Override - public boolean isReadOnly() { - return false; - } - - @Override - public long getTotalSpace() throws IOException { - return 0; - } - - @Override - public long getUsableSpace() throws IOException { - return 0; - } - - @Override - public long getUnallocatedSpace() throws IOException { - return 0; - } - - @Override - public boolean supportsFileAttributeView(Class type) { - return false; - } - - @Override - public boolean supportsFileAttributeView(String name) { - return false; - } - - @Override - public V getFileStoreAttributeView(Class type) { - return null; - } - - @Override - public Object getAttribute(String attribute) throws IOException { - return workspace.getSession().getRepository().getDescriptor(attribute); - } - - public Workspace getWorkspace() { - return workspace; - } - - public String toFsPath(Node node) throws RepositoryException { - String nodeWorkspaceName = node.getSession().getWorkspace().getName(); - if (!nodeWorkspaceName.equals(workspace.getName())) - throw new IllegalArgumentException("Icompatible " + node + " from workspace '" + nodeWorkspaceName - + "' in file store '" + workspace.getName() + "'"); - return mountPath == null ? node.getPath() : mountPath + node.getPath(); - } - - public boolean isBase() { - return mountPath == null; - } - - Node toNode(String[] fullPath) throws RepositoryException { - String jcrPath = toJcrPath(fullPath); - Session session = workspace.getSession(); - if (!session.itemExists(jcrPath)) - return null; - Node node = session.getNode(jcrPath); - return node; - } - - String toJcrPath(String fsPath) { - if (fsPath.length() == 1) - return toJcrPath((String[]) null);// root - String[] arr = fsPath.substring(1).split("/"); -// if (arr.length == 0 || (arr.length == 1 && arr[0].equals(""))) -// return toJcrPath((String[]) null);// root -// else - return toJcrPath(arr); - } - - private String toJcrPath(String[] path) { - if (path == null) - return "/"; - if (path.length < mountDepth) - throw new IllegalArgumentException( - "Path " + Arrays.asList(path) + " is no compatible with mount " + mountPath); - - if (!isBase()) { - // check mount compatibility - StringBuilder mount = new StringBuilder(); - mount.append('/'); - for (int i = 0; i < mountDepth; i++) { - if (i != 0) - mount.append('/'); - mount.append(Text.escapeIllegalJcrChars(path[i])); - } - if (!mountPath.equals(mount.toString())) - throw new IllegalArgumentException( - "Path " + Arrays.asList(path) + " is no compatible with mount " + mountPath); - } - - StringBuilder sb = new StringBuilder(); - sb.append('/'); - for (int i = mountDepth; i < path.length; i++) { - if (i != mountDepth) - sb.append('/'); - sb.append(Text.escapeIllegalJcrChars(path[i])); - } - return sb.toString(); - } - - public String getMountPath() { - return mountPath; - } - - public String getWorkspaceName() { - return workspaceName; - } - - public int getMountDepth() { - return mountDepth; - } - - @Override - public int hashCode() { - return workspaceName.hashCode(); - } - - @Override - public boolean equals(Object obj) { - if (!(obj instanceof WorkspaceFileStore)) - return false; - WorkspaceFileStore other = (WorkspaceFileStore) obj; - return workspaceName.equals(other.workspaceName); - } - - @Override - public String toString() { - return "WorkspaceFileStore " + workspaceName; - } - -} diff --git a/org.argeo.jcr/src/org/argeo/jcr/fs/package-info.java b/org.argeo.jcr/src/org/argeo/jcr/fs/package-info.java deleted file mode 100644 index 0cdfdaf43..000000000 --- a/org.argeo.jcr/src/org/argeo/jcr/fs/package-info.java +++ /dev/null @@ -1,2 +0,0 @@ -/** Java NIO file system implementation based on plain JCR. */ -package org.argeo.jcr.fs; \ No newline at end of file diff --git a/org.argeo.jcr/src/org/argeo/jcr/jcrx.cnd b/org.argeo.jcr/src/org/argeo/jcr/jcrx.cnd deleted file mode 100644 index 3eb0e7a3d..000000000 --- a/org.argeo.jcr/src/org/argeo/jcr/jcrx.cnd +++ /dev/null @@ -1,16 +0,0 @@ -// -// JCR EXTENSIONS -// - - -[jcrx:xmlvalue] -- * -+ jcr:xmltext (jcrx:xmltext) = jcrx:xmltext - -[jcrx:xmltext] - - jcr:xmlcharacters (STRING) mandatory - -[jcrx:csum] -mixin - - jcrx:sum (STRING) * - \ No newline at end of file diff --git a/org.argeo.jcr/src/org/argeo/jcr/package-info.java b/org.argeo.jcr/src/org/argeo/jcr/package-info.java deleted file mode 100644 index 1837749f1..000000000 --- a/org.argeo.jcr/src/org/argeo/jcr/package-info.java +++ /dev/null @@ -1,2 +0,0 @@ -/** Generic JCR utilities. */ -package org.argeo.jcr; \ No newline at end of file diff --git a/org.argeo.jcr/src/org/argeo/jcr/xml/JcrXmlUtils.java b/org.argeo.jcr/src/org/argeo/jcr/xml/JcrXmlUtils.java deleted file mode 100644 index 2adb6a97e..000000000 --- a/org.argeo.jcr/src/org/argeo/jcr/xml/JcrXmlUtils.java +++ /dev/null @@ -1,186 +0,0 @@ -package org.argeo.jcr.xml; - -import java.io.IOException; -import java.io.Writer; -import java.util.Map; -import java.util.TreeMap; - -import javax.jcr.Node; -import javax.jcr.NodeIterator; -import javax.jcr.Property; -import javax.jcr.PropertyIterator; -import javax.jcr.RepositoryException; -import javax.jcr.Value; -import javax.jcr.nodetype.NodeType; - -import org.argeo.jcr.Jcr; - -/** Utilities around JCR and XML. */ -public class JcrXmlUtils { - /** - * Convenience method calling {@link #toXmlElements(Writer, Node, boolean)} with - * false. - */ - public static void toXmlElements(Writer writer, Node node) throws RepositoryException, IOException { - toXmlElements(writer, node, null, false, false, false); - } - - /** - * Write JCR properties as XML elements in a tree structure whose elements are - * named by node primary type. - * - * @param writer the writer to use - * @param node the subtree - * @param depth maximal depth, or if null the whole - * subtree. It must be positive, with depth 0 - * describing just the node without its children. - * @param withMetadata whether to write the primary type and mixins as - * elements - * @param withPrefix whether to keep the namespace prefixes - * @param propertiesAsElements whether single properties should be written as - * elements rather than attributes. If - * false, multiple properties will be - * skipped. - */ - public static void toXmlElements(Writer writer, Node node, Integer depth, boolean withMetadata, boolean withPrefix, - boolean propertiesAsElements) throws RepositoryException, IOException { - if (depth != null && depth < 0) - throw new IllegalArgumentException("Depth " + depth + " is negative."); - - if (node.getName().equals(Jcr.JCR_XMLTEXT)) { - writer.write(node.getProperty(Jcr.JCR_XMLCHARACTERS).getString()); - return; - } - - if (!propertiesAsElements) { - Map attrs = new TreeMap<>(); - PropertyIterator pit = node.getProperties(); - properties: while (pit.hasNext()) { - Property p = pit.nextProperty(); - if (!p.isMultiple()) { - String pName = p.getName(); - if (!withMetadata && (pName.equals(Jcr.JCR_PRIMARY_TYPE) || pName.equals(Jcr.JCR_UUID) - || pName.equals(Jcr.JCR_CREATED) || pName.equals(Jcr.JCR_CREATED_BY) - || pName.equals(Jcr.JCR_LAST_MODIFIED) || pName.equals(Jcr.JCR_LAST_MODIFIED_BY))) - continue properties; - attrs.put(withPrefix(p.getName(), withPrefix), p.getString()); - } - } - if (withMetadata && node.hasProperty(Property.JCR_UUID)) - attrs.put("id", "urn:uuid:" + node.getProperty(Property.JCR_UUID).getString()); - attrs.put(withPrefix ? Jcr.JCR_NAME : "name", node.getName()); - writeStart(writer, withPrefix(node.getPrimaryNodeType().getName(), withPrefix), attrs, node.hasNodes()); - } else { - if (withMetadata && node.hasProperty(Property.JCR_UUID)) { - writeStart(writer, withPrefix(node.getPrimaryNodeType().getName(), withPrefix), "id", - "urn:uuid:" + node.getProperty(Property.JCR_UUID).getString()); - } else { - writeStart(writer, withPrefix(node.getPrimaryNodeType().getName(), withPrefix)); - } - // name - writeStart(writer, withPrefix ? Jcr.JCR_NAME : "name"); - writer.append(node.getName()); - writeEnd(writer, withPrefix ? Jcr.JCR_NAME : "name"); - } - - // mixins - if (withMetadata) { - for (NodeType mixin : node.getMixinNodeTypes()) { - writeStart(writer, withPrefix ? Jcr.JCR_MIXIN_TYPES : "mixinTypes"); - writer.append(mixin.getName()); - writeEnd(writer, withPrefix ? Jcr.JCR_MIXIN_TYPES : "mixinTypes"); - } - } - - // properties as elements - if (propertiesAsElements) { - PropertyIterator pit = node.getProperties(); - properties: while (pit.hasNext()) { - Property p = pit.nextProperty(); - if (p.isMultiple()) { - for (Value value : p.getValues()) { - writeStart(writer, withPrefix(p.getName(), withPrefix)); - writer.write(value.getString()); - writeEnd(writer, withPrefix(p.getName(), withPrefix)); - } - } else { - Value value = p.getValue(); - String pName = p.getName(); - if (!withMetadata && (pName.equals(Jcr.JCR_PRIMARY_TYPE) || pName.equals(Jcr.JCR_UUID) - || pName.equals(Jcr.JCR_CREATED) || pName.equals(Jcr.JCR_CREATED_BY) - || pName.equals(Jcr.JCR_LAST_MODIFIED) || pName.equals(Jcr.JCR_LAST_MODIFIED_BY))) - continue properties; - writeStart(writer, withPrefix(p.getName(), withPrefix)); - writer.write(value.getString()); - writeEnd(writer, withPrefix(p.getName(), withPrefix)); - } - } - } - - // children - if (node.hasNodes()) { - if (depth == null || depth > 0) { - NodeIterator nit = node.getNodes(); - while (nit.hasNext()) { - toXmlElements(writer, nit.nextNode(), depth == null ? null : depth - 1, withMetadata, withPrefix, - propertiesAsElements); - } - } - writeEnd(writer, withPrefix(node.getPrimaryNodeType().getName(), withPrefix)); - } - } - - private static String withPrefix(String str, boolean withPrefix) { - if (withPrefix) - return str; - int index = str.indexOf(':'); - if (index < 0) - return str; - return str.substring(index + 1); - } - - private static void writeStart(Writer writer, String tagName) throws IOException { - writer.append('<'); - writer.append(tagName); - writer.append('>'); - } - - private static void writeStart(Writer writer, String tagName, String attr, String value) throws IOException { - writer.append('<'); - writer.append(tagName); - writer.append(' '); - writer.append(attr); - writer.append("=\""); - writer.append(value); - writer.append("\">"); - } - - private static void writeStart(Writer writer, String tagName, Map attrs, boolean hasChildren) - throws IOException { - writer.append('<'); - writer.append(tagName); - for (String attr : attrs.keySet()) { - writer.append(' '); - writer.append(attr); - writer.append("=\""); - writer.append(attrs.get(attr)); - writer.append('\"'); - } - if (hasChildren) - writer.append('>'); - else - writer.append("/>"); - } - - private static void writeEnd(Writer writer, String tagName) throws IOException { - writer.append("'); - } - - /** Singleton. */ - private JcrXmlUtils() { - - } - -} diff --git a/org.argeo.jcr/src/org/argeo/jcr/xml/removePrefixes.xsl b/org.argeo.jcr/src/org/argeo/jcr/xml/removePrefixes.xsl deleted file mode 100644 index 813d06570..000000000 --- a/org.argeo.jcr/src/org/argeo/jcr/xml/removePrefixes.xsl +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/org.argeo.maintenance/src/org/argeo/maintenance/AbstractMaintenanceService.java b/org.argeo.maintenance/src/org/argeo/maintenance/AbstractMaintenanceService.java deleted file mode 100644 index 6003d638d..000000000 --- a/org.argeo.maintenance/src/org/argeo/maintenance/AbstractMaintenanceService.java +++ /dev/null @@ -1,221 +0,0 @@ -package org.argeo.maintenance; - -import java.io.IOException; -import java.util.EnumSet; -import java.util.HashSet; -import java.util.Set; - -import javax.jcr.NoSuchWorkspaceException; -import javax.jcr.Repository; -import javax.jcr.RepositoryException; -import javax.jcr.Session; -import javax.transaction.UserTransaction; - -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; -import org.argeo.api.NodeUtils; -import org.argeo.jcr.Jcr; -import org.argeo.jcr.JcrUtils; -import org.argeo.naming.Distinguished; -import org.osgi.service.useradmin.Group; -import org.osgi.service.useradmin.Role; -import org.osgi.service.useradmin.UserAdmin; - -/** Make sure roles and access rights are properly configured. */ -public abstract class AbstractMaintenanceService { - private final static Log log = LogFactory.getLog(AbstractMaintenanceService.class); - - private Repository repository; -// private UserAdminService userAdminService; - private UserAdmin userAdmin; - private UserTransaction userTransaction; - - public void init() { - makeSureRolesExists(getRequiredRoles()); - configureStandardRoles(); - - Set workspaceNames = getWorkspaceNames(); - if (workspaceNames == null || workspaceNames.isEmpty()) { - configureJcr(repository, null); - } else { - for (String workspaceName : workspaceNames) - configureJcr(repository, workspaceName); - } - } - - /** Configures a workspace. */ - protected void configureJcr(Repository repository, String workspaceName) { - Session adminSession; - try { - adminSession = NodeUtils.openDataAdminSession(repository, workspaceName); - } catch (RuntimeException e1) { - if (e1.getCause() != null && e1.getCause() instanceof NoSuchWorkspaceException) { - Session defaultAdminSession = NodeUtils.openDataAdminSession(repository, null); - try { - defaultAdminSession.getWorkspace().createWorkspace(workspaceName); - log.info("Created JCR workspace " + workspaceName); - } catch (RepositoryException e) { - throw new IllegalStateException("Cannot create workspace " + workspaceName, e); - } finally { - Jcr.logout(defaultAdminSession); - } - adminSession = NodeUtils.openDataAdminSession(repository, workspaceName); - } else - throw e1; - } - try { - if (prepareJcrTree(adminSession)) { - configurePrivileges(adminSession); - } - } catch (RepositoryException | IOException e) { - throw new IllegalStateException("Cannot initialise JCR data layer.", e); - } finally { - JcrUtils.logoutQuietly(adminSession); - } - } - - /** To be overridden. */ - protected Set getWorkspaceNames() { - return null; - } - - /** - * To be overridden in order to programmatically set relationships between - * roles. Does nothing by default. - */ - protected void configureStandardRoles() { - } - - /** - * Creates the base JCR tree structure expected for this app if necessary. - * - * Expects a clean session ({@link Session#hasPendingChanges()} should return - * false) and saves it once the changes have been done. Thus the session can be - * rolled back if an exception occurs. - * - * @return true if something as been updated - */ - public boolean prepareJcrTree(Session adminSession) throws RepositoryException, IOException { - return false; - } - - /** - * Adds app specific default privileges. - * - * Expects a clean session ({@link Session#hasPendingChanges()} should return - * false} and saves it once the changes have been done. Thus the session can be - * rolled back if an exception occurs. - * - * Warning: no check is done and corresponding privileges are always added, so - * only call this when necessary - */ - public void configurePrivileges(Session session) throws RepositoryException { - } - - /** The system roles that must be available in the system. */ - protected Set getRequiredRoles() { - return new HashSet<>(); - } - - public void destroy() { - - } - - /* - * UTILITIES - */ - - /** Create these roles as group if they don't exist. */ - protected void makeSureRolesExists(EnumSet enumSet) { - makeSureRolesExists(Distinguished.enumToDns(enumSet)); - } - - /** Create these roles as group if they don't exist. */ - protected void makeSureRolesExists(Set requiredRoles) { - if (requiredRoles == null) - return; - if (getUserAdmin() == null) { - log.warn("No user admin service available, cannot make sure that role exists"); - return; - } - for (String role : requiredRoles) { - Role systemRole = getUserAdmin().getRole(role); - if (systemRole == null) { - try { - getUserTransaction().begin(); - getUserAdmin().createRole(role, Role.GROUP); - getUserTransaction().commit(); - log.info("Created role " + role); - } catch (Exception e) { - try { - getUserTransaction().rollback(); - } catch (Exception e1) { - // silent - } - throw new IllegalStateException("Cannot create role " + role, e); - } - } - } - } - - /** Add a user or group to a group. */ - protected void addToGroup(String groupToAddDn, String groupDn) { - if (groupToAddDn.contentEquals(groupDn)) { - if (log.isTraceEnabled()) - log.trace("Ignore adding group " + groupDn + " to itself"); - return; - } - - if (getUserAdmin() == null) { - log.warn("No user admin service available, cannot add group " + groupToAddDn + " to " + groupDn); - return; - } - Group groupToAdd = (Group) getUserAdmin().getRole(groupToAddDn); - if (groupToAdd == null) - throw new IllegalArgumentException("Group " + groupToAddDn + " not found"); - Group group = (Group) getUserAdmin().getRole(groupDn); - if (group == null) - throw new IllegalArgumentException("Group " + groupDn + " not found"); - try { - getUserTransaction().begin(); - if (group.addMember(groupToAdd)) - log.info("Added " + groupToAddDn + " to " + group); - getUserTransaction().commit(); - } catch (Exception e) { - try { - getUserTransaction().rollback(); - } catch (Exception e1) { - // silent - } - throw new IllegalStateException("Cannot add " + groupToAddDn + " to " + groupDn); - } - } - - /* - * DEPENDENCY INJECTION - */ - public void setRepository(Repository repository) { - this.repository = repository; - } - -// public void setUserAdminService(UserAdminService userAdminService) { -// this.userAdminService = userAdminService; -// } - - protected UserTransaction getUserTransaction() { - return userTransaction; - } - - protected UserAdmin getUserAdmin() { - return userAdmin; - } - - public void setUserAdmin(UserAdmin userAdmin) { - this.userAdmin = userAdmin; - } - - public void setUserTransaction(UserTransaction userTransaction) { - this.userTransaction = userTransaction; - } - -} diff --git a/org.argeo.maintenance/src/org/argeo/maintenance/SimpleRoleRegistration.java b/org.argeo.maintenance/src/org/argeo/maintenance/SimpleRoleRegistration.java deleted file mode 100644 index a30fe9796..000000000 --- a/org.argeo.maintenance/src/org/argeo/maintenance/SimpleRoleRegistration.java +++ /dev/null @@ -1,87 +0,0 @@ -package org.argeo.maintenance; - -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - -import javax.naming.InvalidNameException; -import javax.naming.ldap.LdapName; -import javax.transaction.UserTransaction; - -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; -import org.osgi.service.useradmin.Role; -import org.osgi.service.useradmin.UserAdmin; - -/** - * Register one or many roles via a user admin service. Does nothing if the role - * is already registered. - */ -public class SimpleRoleRegistration implements Runnable { - private final static Log log = LogFactory.getLog(SimpleRoleRegistration.class); - - private String role; - private List roles = new ArrayList(); - private UserAdmin userAdmin; - private UserTransaction userTransaction; - - @Override - public void run() { - try { - userTransaction.begin(); - if (role != null && !roleExists(role)) - newRole(toDn(role)); - - for (String r : roles) - if (!roleExists(r)) - newRole(toDn(r)); - userTransaction.commit(); - } catch (Exception e) { - try { - userTransaction.rollback(); - } catch (Exception e1) { - log.error("Cannot rollback", e1); - } - throw new IllegalArgumentException("Cannot add roles", e); - } - } - - private boolean roleExists(String role) { - return userAdmin.getRole(toDn(role).toString()) != null; - } - - protected void newRole(LdapName r) { - userAdmin.createRole(r.toString(), Role.GROUP); - log.info("Added role " + r + " required by application."); - } - - public void register(UserAdmin userAdminService, Map properties) { - this.userAdmin = userAdminService; - run(); - } - - protected LdapName toDn(String name) { - try { - return new LdapName("cn=" + name + ",ou=roles,ou=node"); - } catch (InvalidNameException e) { - throw new IllegalArgumentException("Badly formatted role name " + name, e); - } - } - - public void setRole(String role) { - this.role = role; - } - - public void setRoles(List roles) { - this.roles = roles; - } - - public void setUserAdmin(UserAdmin userAdminService) { - this.userAdmin = userAdminService; - } - - public void setUserTransaction(UserTransaction userTransaction) { - this.userTransaction = userTransaction; - } - -} diff --git a/org.argeo.maintenance/src/org/argeo/maintenance/backup/BackupContentHandler.java b/org.argeo.maintenance/src/org/argeo/maintenance/backup/BackupContentHandler.java deleted file mode 100644 index ef83c1ff9..000000000 --- a/org.argeo.maintenance/src/org/argeo/maintenance/backup/BackupContentHandler.java +++ /dev/null @@ -1,257 +0,0 @@ -package org.argeo.maintenance.backup; - -import java.io.IOException; -import java.io.InputStream; -import java.io.Writer; -import java.util.Arrays; -import java.util.Base64; -import java.util.Set; -import java.util.TreeSet; - -import javax.jcr.Binary; -import javax.jcr.Node; -import javax.jcr.RepositoryException; -import javax.jcr.Session; - -import org.apache.commons.io.IOUtils; -import org.argeo.jcr.Jcr; -import org.argeo.jcr.JcrException; -import org.xml.sax.Attributes; -import org.xml.sax.SAXException; -import org.xml.sax.helpers.DefaultHandler; - -/** XML handler serialising a JCR system view. */ -public class BackupContentHandler extends DefaultHandler { - final static int MAX_DEPTH = 1024; - final static String SV_NAMESPACE_URI = "http://www.jcp.org/jcr/sv/1.0"; - final static String SV_PREFIX = "sv"; - // elements - final static String NODE = "node"; - final static String PROPERTY = "property"; - final static String VALUE = "value"; - // attributes - final static String NAME = "name"; - final static String MULTIPLE = "multiple"; - final static String TYPE = "type"; - - // values - final static String BINARY = "Binary"; - final static String JCR_CONTENT = "jcr:content"; - - private Writer out; - private Session session; - private Set contentPaths = new TreeSet<>(); - - boolean prettyPrint = true; - - private final String parentPath; - -// private boolean inSystem = false; - - public BackupContentHandler(Writer out, Node node) { - super(); - this.out = out; - this.session = Jcr.getSession(node); - parentPath = Jcr.getParentPath(node); - } - - private int currentDepth = -1; - private String[] currentPath = new String[MAX_DEPTH]; - - private boolean currentPropertyIsMultiple = false; - private String currentEncoded = null; - private Base64.Encoder base64encore = Base64.getEncoder(); - - @Override - public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException { - boolean isNode; - boolean isProperty; - switch (localName) { - case NODE: - isNode = true; - isProperty = false; - break; - case PROPERTY: - isNode = false; - isProperty = true; - break; - default: - isNode = false; - isProperty = false; - } - - if (isNode) { - String nodeName = attributes.getValue(SV_NAMESPACE_URI, NAME); - currentDepth = currentDepth + 1; -// if (currentDepth >= 0) - currentPath[currentDepth] = nodeName; -// System.out.println(getCurrentPath() + " , depth=" + currentDepth); -// if ("jcr:system".equals(nodeName)) { -// inSystem = true; -// } - } -// if (inSystem) -// return; - - if (SV_NAMESPACE_URI.equals(uri)) - try { - if (prettyPrint) { - if (isNode) { - out.write(spaces()); - out.write("\n"); - out.write(spaces()); - } else if (isProperty) - out.write(spaces()); - else if (currentPropertyIsMultiple) - out.write(spaces()); - } - - out.write("<"); - out.write(SV_PREFIX + ":" + localName); - if (isProperty) - currentPropertyIsMultiple = false; // always reset - for (int i = 0; i < attributes.getLength(); i++) { - String ns = attributes.getURI(i); - if (SV_NAMESPACE_URI.equals(ns)) { - String attrName = attributes.getLocalName(i); - String attrValue = attributes.getValue(i); - out.write(" "); - out.write(SV_PREFIX + ":" + attrName); - out.write("="); - out.write("\""); - out.write(attrValue); - out.write("\""); - if (isProperty) { - if (MULTIPLE.equals(attrName)) - currentPropertyIsMultiple = Boolean.parseBoolean(attrValue); - else if (TYPE.equals(attrName)) { - if (BINARY.equals(attrValue)) { - if (JCR_CONTENT.equals(getCurrentName())) { - contentPaths.add(getCurrentJcrPath()); - } else { - Binary binary = session.getNode(getCurrentJcrPath()).getProperty(attrName) - .getBinary(); - try (InputStream in = binary.getStream()) { - currentEncoded = base64encore.encodeToString(IOUtils.toByteArray(in)); - } finally { - - } - } - } - } - } - } - } - if (isNode && currentDepth == 0) { - // out.write(" xmlns=\"" + SV_NAMESPACE_URI + "\""); - out.write(" xmlns:" + SV_PREFIX + "=\"" + SV_NAMESPACE_URI + "\""); - } - out.write(">"); - - if (prettyPrint) - if (isNode) - out.write("\n"); - else if (isProperty && currentPropertyIsMultiple) - out.write("\n"); - } catch (IOException e) { - throw new RuntimeException(e); - } catch (RepositoryException e) { - throw new JcrException(e); - } - } - - @Override - public void endElement(String uri, String localName, String qName) throws SAXException { - boolean isNode = localName.equals(NODE); - boolean isValue = localName.equals(VALUE); - if (prettyPrint) - if (!isValue) - try { - if (isNode || currentPropertyIsMultiple) - out.write(spaces()); - } catch (IOException e1) { - throw new RuntimeException(e1); - } - if (isNode) { -// System.out.println("endElement " + getCurrentPath() + " , depth=" + currentDepth); -// if (currentDepth > 0) - currentPath[currentDepth] = null; - currentDepth = currentDepth - 1; -// if (inSystem) { -// // System.out.println("Skip " + getCurrentPath()+" , -// // currentDepth="+currentDepth); -// if (currentDepth == 0) { -// inSystem = false; -// return; -// } -// } - } -// if (inSystem) -// return; - if (SV_NAMESPACE_URI.equals(uri)) - try { - if (isValue && currentEncoded != null) { - out.write(currentEncoded); - } - currentEncoded = null; - out.write(""); - if (prettyPrint) - if (!isValue) - out.write("\n"); - else { - if (currentPropertyIsMultiple) - out.write("\n"); - } - if (currentDepth == 0) - out.flush(); - } catch (IOException e) { - throw new RuntimeException(e); - } - - } - - private char[] spaces() { - char[] arr = new char[currentDepth]; - Arrays.fill(arr, ' '); - return arr; - } - - @Override - public void characters(char[] ch, int start, int length) throws SAXException { -// if (inSystem) -// return; - try { - out.write(ch, start, length); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - protected String getCurrentName() { - assert currentDepth >= 0; -// if (currentDepth == 0) -// return "jcr:root"; - return currentPath[currentDepth]; - } - - protected String getCurrentJcrPath() { -// if (currentDepth == 0) -// return "/"; - StringBuilder sb = new StringBuilder(parentPath.equals("/") ? "" : parentPath); - for (int i = 0; i <= currentDepth; i++) { -// if (i != 0) - sb.append('/'); - sb.append(currentPath[i]); - } - return sb.toString(); - } - - public Set getContentPaths() { - return contentPaths; - } - -} diff --git a/org.argeo.maintenance/src/org/argeo/maintenance/backup/LogicalBackup.java b/org.argeo.maintenance/src/org/argeo/maintenance/backup/LogicalBackup.java deleted file mode 100644 index 60e8f8e5d..000000000 --- a/org.argeo.maintenance/src/org/argeo/maintenance/backup/LogicalBackup.java +++ /dev/null @@ -1,449 +0,0 @@ -package org.argeo.maintenance.backup; - -import java.io.BufferedWriter; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.io.OutputStreamWriter; -import java.io.Writer; -import java.net.URI; -import java.net.URL; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.Dictionary; -import java.util.Enumeration; -import java.util.HashMap; -import java.util.Map; -import java.util.Set; -import java.util.TreeMap; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.Future; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; -import java.util.jar.JarOutputStream; -import java.util.jar.Manifest; -import java.util.zip.ZipEntry; -import java.util.zip.ZipException; -import java.util.zip.ZipOutputStream; - -import javax.jcr.Binary; -import javax.jcr.Node; -import javax.jcr.NodeIterator; -import javax.jcr.Property; -import javax.jcr.Repository; -import javax.jcr.RepositoryException; -import javax.jcr.RepositoryFactory; -import javax.jcr.Session; - -import org.apache.commons.io.IOUtils; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; -import org.apache.jackrabbit.api.JackrabbitSession; -import org.apache.jackrabbit.api.JackrabbitValue; -import org.argeo.api.NodeConstants; -import org.argeo.api.NodeUtils; -import org.argeo.jackrabbit.client.ClientDavexRepositoryFactory; -import org.argeo.jcr.Jcr; -import org.argeo.jcr.JcrException; -import org.argeo.jcr.JcrUtils; -import org.osgi.framework.Bundle; -import org.osgi.framework.BundleContext; - -/** - * Performs a backup of the data based only on programmatic interfaces. Useful - * for migration or live backup. Physical backups of the underlying file - * systems, databases, LDAP servers, etc. should be performed for disaster - * recovery. - */ -public class LogicalBackup implements Runnable { - private final static Log log = LogFactory.getLog(LogicalBackup.class); - - public final static String WORKSPACES_BASE = "workspaces/"; - public final static String FILES_BASE = "files/"; - public final static String OSGI_BASE = "share/osgi/"; - - public final static String JCR_SYSTEM = "jcr:system"; - public final static String JCR_VERSION_STORAGE_PATH = "/jcr:system/jcr:versionStorage"; - - private final Repository repository; - private String defaultWorkspace; - private final BundleContext bundleContext; - - private final ZipOutputStream zout; - private final Path basePath; - - private ExecutorService executorService; - - private boolean performSoftwareBackup = false; - - private Map checksums = new TreeMap<>(); - - private int threadCount = 5; - - private boolean backupFailed = false; - - public LogicalBackup(BundleContext bundleContext, Repository repository, Path basePath) { - this.repository = repository; - this.zout = null; - this.basePath = basePath; - this.bundleContext = bundleContext; - } - - @Override - public void run() { - try { - log.info("Start logical backup to " + basePath); - perform(); - } catch (Exception e) { - log.error("Unexpected exception when performing logical backup", e); - throw new IllegalStateException("Logical backup failed", e); - } - - } - - public void perform() throws RepositoryException, IOException { - if (executorService != null && !executorService.isTerminated()) - throw new IllegalStateException("Another backup is running"); - executorService = Executors.newFixedThreadPool(threadCount); - long begin = System.currentTimeMillis(); - // software backup - if (bundleContext != null && performSoftwareBackup) - executorService.submit(() -> performSoftwareBackup(bundleContext)); - - // data backup - Session defaultSession = login(null); - defaultWorkspace = defaultSession.getWorkspace().getName(); - try { - String[] workspaceNames = defaultSession.getWorkspace().getAccessibleWorkspaceNames(); - workspaces: for (String workspaceName : workspaceNames) { - if ("security".equals(workspaceName)) - continue workspaces; - performDataBackup(workspaceName); - } - } finally { - JcrUtils.logoutQuietly(defaultSession); - executorService.shutdown(); - try { - executorService.awaitTermination(24, TimeUnit.HOURS); - } catch (InterruptedException e) { - // silent - throw new IllegalStateException("Backup was interrupted before completion", e); - } - } - // versions - executorService = Executors.newFixedThreadPool(threadCount); - try { - performVersionsBackup(); - } finally { - executorService.shutdown(); - try { - executorService.awaitTermination(24, TimeUnit.HOURS); - } catch (InterruptedException e) { - // silent - throw new IllegalStateException("Backup was interrupted before completion", e); - } - } - long duration = System.currentTimeMillis() - begin; - if (isBackupFailed()) - log.info("System logical backup failed after " + (duration / 60000) + "min " + (duration / 1000) + "s"); - else - log.info("System logical backup completed in " + (duration / 60000) + "min " + (duration / 1000) + "s"); - } - - protected void performDataBackup(String workspaceName) throws RepositoryException, IOException { - Session session = login(workspaceName); - try { - nodes: for (NodeIterator nit = session.getRootNode().getNodes(); nit.hasNext();) { - if (isBackupFailed()) - return; - Node nodeToExport = nit.nextNode(); - if (JCR_SYSTEM.equals(nodeToExport.getName())) - continue nodes; - String nodePath = nodeToExport.getPath(); - Future> contentPathsFuture = executorService - .submit(() -> performNodeBackup(workspaceName, nodePath)); - executorService.submit(() -> performFilesBackup(workspaceName, contentPathsFuture)); - } - } finally { - Jcr.logout(session); - } - } - - protected void performVersionsBackup() throws RepositoryException, IOException { - Session session = login(defaultWorkspace); - Node versionStorageNode = session.getNode(JCR_VERSION_STORAGE_PATH); - try { - for (NodeIterator nit = versionStorageNode.getNodes(); nit.hasNext();) { - Node nodeToExport = nit.nextNode(); - String nodePath = nodeToExport.getPath(); - if (isBackupFailed()) - return; - Future> contentPathsFuture = executorService - .submit(() -> performNodeBackup(defaultWorkspace, nodePath)); - executorService.submit(() -> performFilesBackup(defaultWorkspace, contentPathsFuture)); - } - } finally { - Jcr.logout(session); - } - - } - - protected Set performNodeBackup(String workspaceName, String nodePath) { - Session session = login(workspaceName); - try { - Node nodeToExport = session.getNode(nodePath); -// String nodeName = nodeToExport.getName(); -// if (nodeName.startsWith("jcr:") || nodeName.startsWith("rep:")) -// continue nodes; -// // TODO make it more robust / configurable -// if (nodeName.equals("user")) -// continue nodes; - String relativePath = WORKSPACES_BASE + workspaceName + nodePath + ".xml"; - OutputStream xmlOut = openOutputStream(relativePath); - BackupContentHandler contentHandler; - try (Writer writer = new BufferedWriter(new OutputStreamWriter(xmlOut, StandardCharsets.UTF_8))) { - contentHandler = new BackupContentHandler(writer, nodeToExport); - session.exportSystemView(nodeToExport.getPath(), contentHandler, true, false); - if (log.isDebugEnabled()) - log.debug(workspaceName + ":" + nodePath + " metadata exported to " + relativePath); - } - - // Files - Set contentPaths = contentHandler.getContentPaths(); - return contentPaths; - } catch (Exception e) { - markBackupFailed("Cannot backup node " + workspaceName + ":" + nodePath, e); - throw new ThreadDeath(); - } finally { - Jcr.logout(session); - } - } - - protected void performFilesBackup(String workspaceName, Future> contentPathsFuture) { - Set contentPaths; - try { - contentPaths = contentPathsFuture.get(24, TimeUnit.HOURS); - } catch (InterruptedException | ExecutionException | TimeoutException e1) { - markBackupFailed("Cannot retrieve content paths for workspace " + workspaceName, e1); - return; - } - if (contentPaths == null || contentPaths.size() == 0) - return; - Session session = login(workspaceName); - try { - String workspacesFilesBasePath = FILES_BASE + workspaceName; - for (String path : contentPaths) { - if (isBackupFailed()) - return; - Node contentNode = session.getNode(path); - Binary binary = null; - try { - binary = contentNode.getProperty(Property.JCR_DATA).getBinary(); - String fileRelativePath = workspacesFilesBasePath + contentNode.getParent().getPath(); - - // checksum - boolean skip = false; - String checksum = null; - if (session instanceof JackrabbitSession) { - JackrabbitValue value = (JackrabbitValue) contentNode.getProperty(Property.JCR_DATA).getValue(); -// ReferenceBinary referenceBinary = (ReferenceBinary) binary; - checksum = value.getContentIdentity(); - } - if (checksum != null) { - if (!checksums.containsKey(checksum)) { - checksums.put(checksum, fileRelativePath); - } else { - skip = true; - String sourcePath = checksums.get(checksum); - if (log.isTraceEnabled()) - log.trace(fileRelativePath + " : already " + sourcePath + " with checksum " + checksum); - createLink(sourcePath, fileRelativePath); - try (Writer writerSum = new OutputStreamWriter( - openOutputStream(fileRelativePath + ".sha256"), StandardCharsets.UTF_8)) { - writerSum.write(checksum); - } - } - } - - // copy file - if (!skip) - try (InputStream in = binary.getStream(); - OutputStream out = openOutputStream(fileRelativePath)) { - IOUtils.copy(in, out); - if (log.isTraceEnabled()) - log.trace("Workspace " + workspaceName + ": file content exported to " - + fileRelativePath); - } - } finally { - JcrUtils.closeQuietly(binary); - } - } - if (log.isDebugEnabled()) - log.debug(workspaceName + ":" + contentPaths.size() + " files exported to " + workspacesFilesBasePath); - } catch (Exception e) { - markBackupFailed("Cannot backup files from " + workspaceName + ":", e); - } finally { - Jcr.logout(session); - } - } - - protected OutputStream openOutputStream(String relativePath) throws IOException { - if (zout != null) { - ZipEntry entry = new ZipEntry(relativePath); - zout.putNextEntry(entry); - return zout; - } else if (basePath != null) { - Path targetPath = basePath.resolve(Paths.get(relativePath)); - Files.createDirectories(targetPath.getParent()); - return Files.newOutputStream(targetPath); - } else { - throw new UnsupportedOperationException(); - } - } - - protected void createLink(String source, String target) throws IOException { - if (zout != null) { - // TODO implement for zip - throw new UnsupportedOperationException(); - } else if (basePath != null) { - Path sourcePath = basePath.resolve(Paths.get(source)); - Path targetPath = basePath.resolve(Paths.get(target)); - Path relativeSource = targetPath.getParent().relativize(sourcePath); - Files.createDirectories(targetPath.getParent()); - Files.createSymbolicLink(targetPath, relativeSource); - } else { - throw new UnsupportedOperationException(); - } - } - - protected void closeOutputStream(String relativePath, OutputStream out) throws IOException { - if (zout != null) { - zout.closeEntry(); - } else if (basePath != null) { - out.close(); - } else { - throw new UnsupportedOperationException(); - } - } - - protected Session login(String workspaceName) { - if (bundleContext != null) {// local - return NodeUtils.openDataAdminSession(repository, workspaceName); - } else {// remote - try { - return repository.login(workspaceName); - } catch (RepositoryException e) { - throw new JcrException(e); - } - } - } - - public final static void main(String[] args) throws Exception { - if (args.length == 0) { - printUsage("No argument"); - System.exit(1); - } - URI uri = new URI(args[0]); - Repository repository = createRemoteRepository(uri); - Path basePath = args.length > 1 ? Paths.get(args[1]) : Paths.get(System.getProperty("user.dir")); - if (!Files.exists(basePath)) - Files.createDirectories(basePath); - LogicalBackup backup = new LogicalBackup(null, repository, basePath); - backup.run(); - } - - private static void printUsage(String errorMessage) { - if (errorMessage != null) - System.err.println(errorMessage); - System.out.println("Usage: LogicalBackup []"); - - } - - protected static Repository createRemoteRepository(URI uri) throws RepositoryException { - RepositoryFactory repositoryFactory = new ClientDavexRepositoryFactory(); - Map params = new HashMap(); - params.put(ClientDavexRepositoryFactory.JACKRABBIT_DAVEX_URI, uri.toString()); - // TODO make it configurable - params.put(ClientDavexRepositoryFactory.JACKRABBIT_REMOTE_DEFAULT_WORKSPACE, NodeConstants.SYS_WORKSPACE); - return repositoryFactory.getRepository(params); - } - - public void performSoftwareBackup(BundleContext bundleContext) { - String bootBasePath = OSGI_BASE + "boot"; - Bundle[] bundles = bundleContext.getBundles(); - for (Bundle bundle : bundles) { - String relativePath = bootBasePath + "/" + bundle.getSymbolicName() + ".jar"; - Dictionary headers = bundle.getHeaders(); - Manifest manifest = new Manifest(); - Enumeration headerKeys = headers.keys(); - while (headerKeys.hasMoreElements()) { - String headerKey = headerKeys.nextElement(); - String headerValue = headers.get(headerKey); - manifest.getMainAttributes().putValue(headerKey, headerValue); - } - try (JarOutputStream jarOut = new JarOutputStream(openOutputStream(relativePath), manifest)) { - Enumeration resourcePaths = bundle.findEntries("/", "*", true); - resources: while (resourcePaths.hasMoreElements()) { - URL entryUrl = resourcePaths.nextElement(); - String entryPath = entryUrl.getPath(); - if (entryPath.equals("")) - continue resources; - if (entryPath.endsWith("/")) - continue resources; - String entryName = entryPath.substring(1);// remove first '/' - if (entryUrl.getPath().equals("/META-INF/")) - continue resources; - if (entryUrl.getPath().equals("/META-INF/MANIFEST.MF")) - continue resources; - // dev - if (entryUrl.getPath().startsWith("/target")) - continue resources; - if (entryUrl.getPath().startsWith("/src")) - continue resources; - if (entryUrl.getPath().startsWith("/ext")) - continue resources; - - if (entryName.startsWith("bin/")) {// dev - entryName = entryName.substring("bin/".length()); - } - - ZipEntry entry = new ZipEntry(entryName); - try (InputStream in = entryUrl.openStream()) { - try { - jarOut.putNextEntry(entry); - } catch (ZipException e) {// duplicate - continue resources; - } - IOUtils.copy(in, jarOut); - jarOut.closeEntry(); -// log.info(entryUrl); - } catch (FileNotFoundException e) { - log.warn(entryUrl + ": " + e.getMessage()); - } - } - } catch (IOException e1) { - throw new RuntimeException("Cannot export bundle " + bundle, e1); - } - } - if (log.isDebugEnabled()) - log.debug(bundles.length + " OSGi bundles exported to " + bootBasePath); - - } - - protected synchronized void markBackupFailed(Object message, Exception e) { - log.error(message, e); - backupFailed = true; - notifyAll(); - if (executorService != null) - executorService.shutdownNow(); - } - - protected boolean isBackupFailed() { - return backupFailed; - } -} diff --git a/org.argeo.maintenance/src/org/argeo/maintenance/backup/LogicalRestore.java b/org.argeo.maintenance/src/org/argeo/maintenance/backup/LogicalRestore.java deleted file mode 100644 index a12bb41c9..000000000 --- a/org.argeo.maintenance/src/org/argeo/maintenance/backup/LogicalRestore.java +++ /dev/null @@ -1,86 +0,0 @@ -package org.argeo.maintenance.backup; - -import java.io.IOException; -import java.io.InputStream; -import java.nio.file.DirectoryStream; -import java.nio.file.Files; -import java.nio.file.Path; - -import javax.jcr.ImportUUIDBehavior; -import javax.jcr.Repository; -import javax.jcr.RepositoryException; -import javax.jcr.Session; - -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; -import org.argeo.api.NodeConstants; -import org.argeo.api.NodeUtils; -import org.argeo.jcr.Jcr; -import org.argeo.jcr.JcrException; -import org.argeo.jcr.JcrUtils; -import org.osgi.framework.BundleContext; - -/** Restores a backup in the format defined by {@link LogicalBackup}. */ -public class LogicalRestore implements Runnable { - private final static Log log = LogFactory.getLog(LogicalRestore.class); - - private final Repository repository; - private final BundleContext bundleContext; - private final Path basePath; - - public LogicalRestore(BundleContext bundleContext, Repository repository, Path basePath) { - this.repository = repository; - this.basePath = basePath; - this.bundleContext = bundleContext; - } - - @Override - public void run() { - Path workspaces = basePath.resolve(LogicalBackup.WORKSPACES_BASE); - try { - // import jcr:system first -// Session defaultSession = NodeUtils.openDataAdminSession(repository, null); -// try (DirectoryStream xmls = Files.newDirectoryStream( -// workspaces.resolve(NodeConstants.SYS_WORKSPACE + LogicalBackup.JCR_VERSION_STORAGE_PATH), -// "*.xml")) { -// for (Path xml : xmls) { -// try (InputStream in = Files.newInputStream(xml)) { -// defaultSession.getWorkspace().importXML(LogicalBackup.JCR_VERSION_STORAGE_PATH, in, -// ImportUUIDBehavior.IMPORT_UUID_COLLISION_REPLACE_EXISTING); -// if (log.isDebugEnabled()) -// log.debug("Restored " + xml + " to " + defaultSession.getWorkspace().getName() + ":"); -// } -// } -// } finally { -// Jcr.logout(defaultSession); -// } - - // non-system content - try (DirectoryStream workspaceDirs = Files.newDirectoryStream(workspaces)) { - for (Path workspacePath : workspaceDirs) { - String workspaceName = workspacePath.getFileName().toString(); - Session session = JcrUtils.loginOrCreateWorkspace(repository, workspaceName); - try (DirectoryStream xmls = Files.newDirectoryStream(workspacePath, "*.xml")) { - xmls: for (Path xml : xmls) { - if (xml.getFileName().toString().startsWith("rep:")) - continue xmls; - try (InputStream in = Files.newInputStream(xml)) { - session.getWorkspace().importXML("/", in, - ImportUUIDBehavior.IMPORT_UUID_COLLISION_REPLACE_EXISTING); - if (log.isDebugEnabled()) - log.debug("Restored " + xml + " to workspace " + workspaceName); - } - } - } finally { - Jcr.logout(session); - } - } - } - } catch (IOException e) { - throw new RuntimeException("Cannot restore backup from " + basePath, e); - } catch (RepositoryException e) { - throw new JcrException("Cannot restore backup from " + basePath, e); - } - } - -} diff --git a/org.argeo.maintenance/src/org/argeo/maintenance/backup/package-info.java b/org.argeo.maintenance/src/org/argeo/maintenance/backup/package-info.java deleted file mode 100644 index a61e19bd4..000000000 --- a/org.argeo.maintenance/src/org/argeo/maintenance/backup/package-info.java +++ /dev/null @@ -1,2 +0,0 @@ -/** Argeo Node backup utilities. */ -package org.argeo.maintenance.backup; \ No newline at end of file diff --git a/org.argeo.maintenance/src/org/argeo/maintenance/internal/Activator.java b/org.argeo.maintenance/src/org/argeo/maintenance/internal/Activator.java deleted file mode 100644 index ef40ab3a3..000000000 --- a/org.argeo.maintenance/src/org/argeo/maintenance/internal/Activator.java +++ /dev/null @@ -1,27 +0,0 @@ -package org.argeo.maintenance.internal; - -import java.nio.file.Path; -import java.nio.file.Paths; - -import javax.jcr.Repository; - -import org.argeo.maintenance.backup.LogicalBackup; -import org.osgi.framework.BundleActivator; -import org.osgi.framework.BundleContext; - -public class Activator implements BundleActivator { - - @Override - public void start(BundleContext context) throws Exception { - // Start backup - Repository repository = context.getService(context.getServiceReference(Repository.class)); - Path basePath = Paths.get(System.getProperty("user.dir"), "backup"); - LogicalBackup backup = new LogicalBackup(context, repository, basePath); - backup.run(); - } - - @Override - public void stop(BundleContext context) throws Exception { - } - -} diff --git a/org.argeo.maintenance/src/org/argeo/maintenance/package-info.java b/org.argeo.maintenance/src/org/argeo/maintenance/package-info.java deleted file mode 100644 index 1ce974c6f..000000000 --- a/org.argeo.maintenance/src/org/argeo/maintenance/package-info.java +++ /dev/null @@ -1,2 +0,0 @@ -/** Utilities for the maintenance of an Argeo Node. */ -package org.argeo.maintenance; \ No newline at end of file diff --git a/pom.xml b/pom.xml index d6f35ab9c..c4d70f610 100644 --- a/pom.xml +++ b/pom.xml @@ -19,7 +19,7 @@ org.argeo.enterprise - org.argeo.jcr + org.argeo.osgi.boot org.argeo.core @@ -27,8 +27,9 @@ org.argeo.eclipse.ui.rap org.argeo.api - org.argeo.maintenance + org.argeo.cms + org.argeo.cms.jcr org.argeo.cms.ui.theme org.argeo.cms.ui org.argeo.cms.ui.rap