Put all JCR projects together.
authorMathieu Baudier <mbaudier@argeo.org>
Sun, 5 Dec 2021 05:16:18 +0000 (06:16 +0100)
committerMathieu Baudier <mbaudier@argeo.org>
Sun, 5 Dec 2021 05:16:18 +0000 (06:16 +0100)
169 files changed:
dep/org.argeo.dep.cms.client/pom.xml
dep/org.argeo.dep.cms.node/pom.xml
org.argeo.cms.jcr/.classpath [new file with mode: 0644]
org.argeo.cms.jcr/.project [new file with mode: 0644]
org.argeo.cms.jcr/.settings/org.eclipse.jdt.core.prefs [new file with mode: 0644]
org.argeo.cms.jcr/bnd.bnd [new file with mode: 0644]
org.argeo.cms.jcr/build.properties [new file with mode: 0644]
org.argeo.cms.jcr/ext/test/org/argeo/jcr/fs/JcrFileSystemTest.java [new file with mode: 0644]
org.argeo.cms.jcr/pom.xml [new file with mode: 0644]
org.argeo.cms.jcr/src/org/argeo/cli/jcr/JcrCommands.java [new file with mode: 0644]
org.argeo.cms.jcr/src/org/argeo/cli/jcr/JcrSync.java [new file with mode: 0644]
org.argeo.cms.jcr/src/org/argeo/cli/jcr/package-info.java [new file with mode: 0644]
org.argeo.cms.jcr/src/org/argeo/cli/jcr/repository-localfs.xml [new file with mode: 0644]
org.argeo.cms.jcr/src/org/argeo/jackrabbit/JackrabbitAdminLoginModule.java [new file with mode: 0644]
org.argeo.cms.jcr/src/org/argeo/jackrabbit/JackrabbitDataModelMigration.java [new file with mode: 0644]
org.argeo.cms.jcr/src/org/argeo/jackrabbit/client/ClientDavexRepositoryFactory.java [new file with mode: 0644]
org.argeo.cms.jcr/src/org/argeo/jackrabbit/client/ClientDavexRepositoryService.java [new file with mode: 0644]
org.argeo.cms.jcr/src/org/argeo/jackrabbit/client/ClientDavexRepositoryServiceFactory.java [new file with mode: 0644]
org.argeo.cms.jcr/src/org/argeo/jackrabbit/client/JackrabbitClient.java [new file with mode: 0644]
org.argeo.cms.jcr/src/org/argeo/jackrabbit/client/NonSerialBasicAuthCache.java [new file with mode: 0644]
org.argeo.cms.jcr/src/org/argeo/jackrabbit/fs/AbstractJackrabbitFsProvider.java [new file with mode: 0644]
org.argeo.cms.jcr/src/org/argeo/jackrabbit/fs/DavexFsProvider.java [new file with mode: 0644]
org.argeo.cms.jcr/src/org/argeo/jackrabbit/fs/JackrabbitMemoryFsProvider.java [new file with mode: 0644]
org.argeo.cms.jcr/src/org/argeo/jackrabbit/fs/fs-memory.xml [new file with mode: 0644]
org.argeo.cms.jcr/src/org/argeo/jackrabbit/fs/package-info.java [new file with mode: 0644]
org.argeo.cms.jcr/src/org/argeo/jackrabbit/package-info.java [new file with mode: 0644]
org.argeo.cms.jcr/src/org/argeo/jackrabbit/repository-h2.xml [new file with mode: 0644]
org.argeo.cms.jcr/src/org/argeo/jackrabbit/repository-localfs.xml [new file with mode: 0644]
org.argeo.cms.jcr/src/org/argeo/jackrabbit/repository-memory.xml [new file with mode: 0644]
org.argeo.cms.jcr/src/org/argeo/jackrabbit/repository-postgresql-ds.xml [new file with mode: 0644]
org.argeo.cms.jcr/src/org/argeo/jackrabbit/repository-postgresql.xml [new file with mode: 0644]
org.argeo.cms.jcr/src/org/argeo/jackrabbit/security/JackrabbitSecurityUtils.java [new file with mode: 0644]
org.argeo.cms.jcr/src/org/argeo/jackrabbit/security/package-info.java [new file with mode: 0644]
org.argeo.cms.jcr/src/org/argeo/jackrabbit/unit/AbstractJackrabbitTestCase.java [new file with mode: 0644]
org.argeo.cms.jcr/src/org/argeo/jackrabbit/unit/jaas.config [new file with mode: 0644]
org.argeo.cms.jcr/src/org/argeo/jackrabbit/unit/package-info.java [new file with mode: 0644]
org.argeo.cms.jcr/src/org/argeo/jackrabbit/unit/repository-h2.xml [new file with mode: 0644]
org.argeo.cms.jcr/src/org/argeo/jackrabbit/unit/repository-memory.xml [new file with mode: 0644]
org.argeo.cms.jcr/src/org/argeo/jcr/Bin.java [new file with mode: 0644]
org.argeo.cms.jcr/src/org/argeo/jcr/CollectionNodeIterator.java [new file with mode: 0644]
org.argeo.cms.jcr/src/org/argeo/jcr/DefaultJcrListener.java [new file with mode: 0644]
org.argeo.cms.jcr/src/org/argeo/jcr/Jcr.java [new file with mode: 0644]
org.argeo.cms.jcr/src/org/argeo/jcr/JcrAuthorizations.java [new file with mode: 0644]
org.argeo.cms.jcr/src/org/argeo/jcr/JcrCallback.java [new file with mode: 0644]
org.argeo.cms.jcr/src/org/argeo/jcr/JcrException.java [new file with mode: 0644]
org.argeo.cms.jcr/src/org/argeo/jcr/JcrMonitor.java [new file with mode: 0644]
org.argeo.cms.jcr/src/org/argeo/jcr/JcrRepositoryWrapper.java [new file with mode: 0644]
org.argeo.cms.jcr/src/org/argeo/jcr/JcrUrlStreamHandler.java [new file with mode: 0644]
org.argeo.cms.jcr/src/org/argeo/jcr/JcrUtils.java [new file with mode: 0644]
org.argeo.cms.jcr/src/org/argeo/jcr/JcrxApi.java [new file with mode: 0644]
org.argeo.cms.jcr/src/org/argeo/jcr/JcrxName.java [new file with mode: 0644]
org.argeo.cms.jcr/src/org/argeo/jcr/JcrxType.java [new file with mode: 0644]
org.argeo.cms.jcr/src/org/argeo/jcr/PropertyDiff.java [new file with mode: 0644]
org.argeo.cms.jcr/src/org/argeo/jcr/SimplePrincipal.java [new file with mode: 0644]
org.argeo.cms.jcr/src/org/argeo/jcr/ThreadBoundJcrSessionFactory.java [new file with mode: 0644]
org.argeo.cms.jcr/src/org/argeo/jcr/VersionDiff.java [new file with mode: 0644]
org.argeo.cms.jcr/src/org/argeo/jcr/fs/BinaryChannel.java [new file with mode: 0644]
org.argeo.cms.jcr/src/org/argeo/jcr/fs/JcrBasicfileAttributes.java [new file with mode: 0644]
org.argeo.cms.jcr/src/org/argeo/jcr/fs/JcrFileSystem.java [new file with mode: 0644]
org.argeo.cms.jcr/src/org/argeo/jcr/fs/JcrFileSystemProvider.java [new file with mode: 0644]
org.argeo.cms.jcr/src/org/argeo/jcr/fs/JcrFsException.java [new file with mode: 0644]
org.argeo.cms.jcr/src/org/argeo/jcr/fs/JcrPath.java [new file with mode: 0644]
org.argeo.cms.jcr/src/org/argeo/jcr/fs/NodeDirectoryStream.java [new file with mode: 0644]
org.argeo.cms.jcr/src/org/argeo/jcr/fs/NodeFileAttributes.java [new file with mode: 0644]
org.argeo.cms.jcr/src/org/argeo/jcr/fs/Text.java [new file with mode: 0644]
org.argeo.cms.jcr/src/org/argeo/jcr/fs/WorkspaceFileStore.java [new file with mode: 0644]
org.argeo.cms.jcr/src/org/argeo/jcr/fs/package-info.java [new file with mode: 0644]
org.argeo.cms.jcr/src/org/argeo/jcr/jcrx.cnd [new file with mode: 0644]
org.argeo.cms.jcr/src/org/argeo/jcr/package-info.java [new file with mode: 0644]
org.argeo.cms.jcr/src/org/argeo/jcr/proxy/AbstractUrlProxy.java [new file with mode: 0644]
org.argeo.cms.jcr/src/org/argeo/jcr/proxy/ResourceProxy.java [new file with mode: 0644]
org.argeo.cms.jcr/src/org/argeo/jcr/proxy/ResourceProxyServlet.java [new file with mode: 0644]
org.argeo.cms.jcr/src/org/argeo/jcr/proxy/package-info.java [new file with mode: 0644]
org.argeo.cms.jcr/src/org/argeo/jcr/unit/AbstractJcrTestCase.java [new file with mode: 0644]
org.argeo.cms.jcr/src/org/argeo/jcr/unit/package-info.java [new file with mode: 0644]
org.argeo.cms.jcr/src/org/argeo/jcr/xml/JcrXmlUtils.java [new file with mode: 0644]
org.argeo.cms.jcr/src/org/argeo/jcr/xml/removePrefixes.xsl [new file with mode: 0644]
org.argeo.cms.jcr/src/org/argeo/maintenance/AbstractMaintenanceService.java [new file with mode: 0644]
org.argeo.cms.jcr/src/org/argeo/maintenance/SimpleRoleRegistration.java [new file with mode: 0644]
org.argeo.cms.jcr/src/org/argeo/maintenance/backup/BackupContentHandler.java [new file with mode: 0644]
org.argeo.cms.jcr/src/org/argeo/maintenance/backup/LogicalBackup.java [new file with mode: 0644]
org.argeo.cms.jcr/src/org/argeo/maintenance/backup/LogicalRestore.java [new file with mode: 0644]
org.argeo.cms.jcr/src/org/argeo/maintenance/backup/package-info.java [new file with mode: 0644]
org.argeo.cms.jcr/src/org/argeo/maintenance/internal/Activator.java [new file with mode: 0644]
org.argeo.cms.jcr/src/org/argeo/maintenance/package-info.java [new file with mode: 0644]
org.argeo.cms/pom.xml
org.argeo.core/.settings/org.eclipse.jdt.core.prefs [deleted file]
org.argeo.core/bnd.bnd
org.argeo.core/build.properties
org.argeo.core/ext/test/org/argeo/jcr/fs/JcrFileSystemTest.java [deleted file]
org.argeo.core/pom.xml
org.argeo.core/src/org/argeo/cli/fs/PathSync.java
org.argeo.core/src/org/argeo/cli/jcr/JcrCommands.java [deleted file]
org.argeo.core/src/org/argeo/cli/jcr/JcrSync.java [deleted file]
org.argeo.core/src/org/argeo/cli/jcr/package-info.java [deleted file]
org.argeo.core/src/org/argeo/cli/jcr/repository-localfs.xml [deleted file]
org.argeo.core/src/org/argeo/jackrabbit/JackrabbitAdminLoginModule.java [deleted file]
org.argeo.core/src/org/argeo/jackrabbit/JackrabbitDataModelMigration.java [deleted file]
org.argeo.core/src/org/argeo/jackrabbit/client/ClientDavexRepositoryFactory.java [deleted file]
org.argeo.core/src/org/argeo/jackrabbit/client/ClientDavexRepositoryService.java [deleted file]
org.argeo.core/src/org/argeo/jackrabbit/client/ClientDavexRepositoryServiceFactory.java [deleted file]
org.argeo.core/src/org/argeo/jackrabbit/client/JackrabbitClient.java [deleted file]
org.argeo.core/src/org/argeo/jackrabbit/client/NonSerialBasicAuthCache.java [deleted file]
org.argeo.core/src/org/argeo/jackrabbit/fs/AbstractJackrabbitFsProvider.java [deleted file]
org.argeo.core/src/org/argeo/jackrabbit/fs/DavexFsProvider.java [deleted file]
org.argeo.core/src/org/argeo/jackrabbit/fs/JackrabbitMemoryFsProvider.java [deleted file]
org.argeo.core/src/org/argeo/jackrabbit/fs/fs-memory.xml [deleted file]
org.argeo.core/src/org/argeo/jackrabbit/fs/package-info.java [deleted file]
org.argeo.core/src/org/argeo/jackrabbit/package-info.java [deleted file]
org.argeo.core/src/org/argeo/jackrabbit/repository-h2.xml [deleted file]
org.argeo.core/src/org/argeo/jackrabbit/repository-localfs.xml [deleted file]
org.argeo.core/src/org/argeo/jackrabbit/repository-memory.xml [deleted file]
org.argeo.core/src/org/argeo/jackrabbit/repository-postgresql-ds.xml [deleted file]
org.argeo.core/src/org/argeo/jackrabbit/repository-postgresql.xml [deleted file]
org.argeo.core/src/org/argeo/jackrabbit/security/JackrabbitSecurityUtils.java [deleted file]
org.argeo.core/src/org/argeo/jackrabbit/security/package-info.java [deleted file]
org.argeo.core/src/org/argeo/jackrabbit/unit/AbstractJackrabbitTestCase.java [deleted file]
org.argeo.core/src/org/argeo/jackrabbit/unit/jaas.config [deleted file]
org.argeo.core/src/org/argeo/jackrabbit/unit/package-info.java [deleted file]
org.argeo.core/src/org/argeo/jackrabbit/unit/repository-h2.xml [deleted file]
org.argeo.core/src/org/argeo/jackrabbit/unit/repository-memory.xml [deleted file]
org.argeo.core/src/org/argeo/jcr/proxy/AbstractUrlProxy.java [deleted file]
org.argeo.core/src/org/argeo/jcr/proxy/ResourceProxy.java [deleted file]
org.argeo.core/src/org/argeo/jcr/proxy/ResourceProxyServlet.java [deleted file]
org.argeo.core/src/org/argeo/jcr/proxy/package-info.java [deleted file]
org.argeo.core/src/org/argeo/jcr/unit/AbstractJcrTestCase.java [deleted file]
org.argeo.core/src/org/argeo/jcr/unit/package-info.java [deleted file]
org.argeo.jcr/src/org/argeo/jcr/Bin.java [deleted file]
org.argeo.jcr/src/org/argeo/jcr/CollectionNodeIterator.java [deleted file]
org.argeo.jcr/src/org/argeo/jcr/DefaultJcrListener.java [deleted file]
org.argeo.jcr/src/org/argeo/jcr/Jcr.java [deleted file]
org.argeo.jcr/src/org/argeo/jcr/JcrAuthorizations.java [deleted file]
org.argeo.jcr/src/org/argeo/jcr/JcrCallback.java [deleted file]
org.argeo.jcr/src/org/argeo/jcr/JcrException.java [deleted file]
org.argeo.jcr/src/org/argeo/jcr/JcrMonitor.java [deleted file]
org.argeo.jcr/src/org/argeo/jcr/JcrRepositoryWrapper.java [deleted file]
org.argeo.jcr/src/org/argeo/jcr/JcrUrlStreamHandler.java [deleted file]
org.argeo.jcr/src/org/argeo/jcr/JcrUtils.java [deleted file]
org.argeo.jcr/src/org/argeo/jcr/JcrxApi.java [deleted file]
org.argeo.jcr/src/org/argeo/jcr/JcrxName.java [deleted file]
org.argeo.jcr/src/org/argeo/jcr/JcrxType.java [deleted file]
org.argeo.jcr/src/org/argeo/jcr/PropertyDiff.java [deleted file]
org.argeo.jcr/src/org/argeo/jcr/SimplePrincipal.java [deleted file]
org.argeo.jcr/src/org/argeo/jcr/ThreadBoundJcrSessionFactory.java [deleted file]
org.argeo.jcr/src/org/argeo/jcr/VersionDiff.java [deleted file]
org.argeo.jcr/src/org/argeo/jcr/fs/BinaryChannel.java [deleted file]
org.argeo.jcr/src/org/argeo/jcr/fs/JcrBasicfileAttributes.java [deleted file]
org.argeo.jcr/src/org/argeo/jcr/fs/JcrFileSystem.java [deleted file]
org.argeo.jcr/src/org/argeo/jcr/fs/JcrFileSystemProvider.java [deleted file]
org.argeo.jcr/src/org/argeo/jcr/fs/JcrFsException.java [deleted file]
org.argeo.jcr/src/org/argeo/jcr/fs/JcrPath.java [deleted file]
org.argeo.jcr/src/org/argeo/jcr/fs/NodeDirectoryStream.java [deleted file]
org.argeo.jcr/src/org/argeo/jcr/fs/NodeFileAttributes.java [deleted file]
org.argeo.jcr/src/org/argeo/jcr/fs/Text.java [deleted file]
org.argeo.jcr/src/org/argeo/jcr/fs/WorkspaceFileStore.java [deleted file]
org.argeo.jcr/src/org/argeo/jcr/fs/package-info.java [deleted file]
org.argeo.jcr/src/org/argeo/jcr/jcrx.cnd [deleted file]
org.argeo.jcr/src/org/argeo/jcr/package-info.java [deleted file]
org.argeo.jcr/src/org/argeo/jcr/xml/JcrXmlUtils.java [deleted file]
org.argeo.jcr/src/org/argeo/jcr/xml/removePrefixes.xsl [deleted file]
org.argeo.maintenance/src/org/argeo/maintenance/AbstractMaintenanceService.java [deleted file]
org.argeo.maintenance/src/org/argeo/maintenance/SimpleRoleRegistration.java [deleted file]
org.argeo.maintenance/src/org/argeo/maintenance/backup/BackupContentHandler.java [deleted file]
org.argeo.maintenance/src/org/argeo/maintenance/backup/LogicalBackup.java [deleted file]
org.argeo.maintenance/src/org/argeo/maintenance/backup/LogicalRestore.java [deleted file]
org.argeo.maintenance/src/org/argeo/maintenance/backup/package-info.java [deleted file]
org.argeo.maintenance/src/org/argeo/maintenance/internal/Activator.java [deleted file]
org.argeo.maintenance/src/org/argeo/maintenance/package-info.java [deleted file]
pom.xml

index 423b9b32cfe55e6d8542de183b4e236d82d9ca06..2381d27b363e2b1644e4020ecd72c22acc004bfb 100644 (file)
                        <artifactId>org.argeo.enterprise</artifactId>
                        <version>2.3-SNAPSHOT</version>
                </dependency>
-               <dependency>
-                       <groupId>org.argeo.commons</groupId>
-                       <artifactId>org.argeo.jcr</artifactId>
-                       <version>2.3-SNAPSHOT</version>
-               </dependency>
+<!--           <dependency> -->
+<!--                   <groupId>org.argeo.commons</groupId> -->
+<!--                   <artifactId>org.argeo.jcr</artifactId> -->
+<!--                   <version>2.3-SNAPSHOT</version> -->
+<!--           </dependency> -->
                <dependency>
                        <groupId>org.argeo.commons</groupId>
                        <artifactId>org.argeo.core</artifactId>
index f88a1ded9599ea049724550c0776ab85f23810d4..4afc1f41a7a54331dd74a5bec3923f6e3f2a3067 100644 (file)
@@ -9,6 +9,7 @@
        </parent>
        <artifactId>org.argeo.dep.cms.node</artifactId>
        <name>CMS Node</name>
+       
        <dependencies>
 
                <!-- Parent dependencies -->
                        <version>2.3-SNAPSHOT</version>
                </dependency>
                <dependency>
+               
                        <groupId>org.argeo.commons</groupId>
                        <artifactId>org.argeo.cms</artifactId>
                        <version>2.3-SNAPSHOT</version>
                </dependency>
                <dependency>
                        <groupId>org.argeo.commons</groupId>
-                       <artifactId>org.argeo.maintenance</artifactId>
+                       <artifactId>org.argeo.cms.jcr</artifactId>
                        <version>2.3-SNAPSHOT</version>
                </dependency>
+<!--           <dependency> -->
+<!--                   <groupId>org.argeo.commons</groupId> -->
+<!--                   <artifactId>org.argeo.maintenance</artifactId> -->
+<!--                   <version>2.3-SNAPSHOT</version> -->
+<!--           </dependency> -->
 
                <!-- CMS Dependencies -->
                <!-- <dependency> -->
diff --git a/org.argeo.cms.jcr/.classpath b/org.argeo.cms.jcr/.classpath
new file mode 100644 (file)
index 0000000..20cad80
--- /dev/null
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<classpath>
+       <classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-11">
+               <attributes>
+                       <attribute name="module" value="true"/>
+               </attributes>
+       </classpathentry>
+       <classpathentry kind="con" path="org.eclipse.pde.core.requiredPlugins"/>
+       <classpathentry kind="src" path="src"/>
+       <classpathentry kind="src" path="ext/test"/>
+       <classpathentry kind="output" path="bin"/>
+</classpath>
diff --git a/org.argeo.cms.jcr/.project b/org.argeo.cms.jcr/.project
new file mode 100644 (file)
index 0000000..9b33a44
--- /dev/null
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<projectDescription>
+       <name>org.argeo.cms.jcr</name>
+       <comment></comment>
+       <projects>
+       </projects>
+       <buildSpec>
+               <buildCommand>
+                       <name>org.eclipse.jdt.core.javabuilder</name>
+                       <arguments>
+                       </arguments>
+               </buildCommand>
+               <buildCommand>
+                       <name>org.eclipse.pde.ManifestBuilder</name>
+                       <arguments>
+                       </arguments>
+               </buildCommand>
+               <buildCommand>
+                       <name>org.eclipse.pde.SchemaBuilder</name>
+                       <arguments>
+                       </arguments>
+               </buildCommand>
+       </buildSpec>
+       <natures>
+               <nature>org.eclipse.pde.PluginNature</nature>
+               <nature>org.eclipse.jdt.core.javanature</nature>
+       </natures>
+</projectDescription>
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 (file)
index 0000000..7e2e119
--- /dev/null
@@ -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 (file)
index 0000000..5a8e8ab
--- /dev/null
@@ -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 (file)
index 0000000..8667a0e
--- /dev/null
@@ -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 (file)
index 0000000..2d03b8f
--- /dev/null
@@ -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<Path> 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<Path> 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<String, Object> 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<Path> 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 (file)
index 0000000..396cabe
--- /dev/null
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+       <modelVersion>4.0.0</modelVersion>
+       <parent>
+               <groupId>org.argeo.commons</groupId>
+               <artifactId>argeo-commons</artifactId>
+               <version>2.3-SNAPSHOT</version>
+               <relativePath>..</relativePath>
+       </parent>
+       <artifactId>org.argeo.cms.jcr</artifactId>
+       <name>Commons CMS JCR</name>
+       <dependencies>
+               <dependency>
+                       <groupId>org.argeo.commons</groupId>
+                       <artifactId>org.argeo.api</artifactId>
+                       <version>2.3-SNAPSHOT</version>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.commons</groupId>
+                       <artifactId>org.argeo.enterprise</artifactId>
+                       <version>2.3-SNAPSHOT</version>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.commons</groupId>
+                       <artifactId>org.argeo.core</artifactId>
+                       <version>2.3-SNAPSHOT</version>
+               </dependency>
+       </dependencies>
+</project>
\ 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 (file)
index 0000000..ea74674
--- /dev/null
@@ -0,0 +1,18 @@
+package org.argeo.cli.jcr;
+
+import org.argeo.cli.CommandsCli;
+
+/** File utilities. */
+public class JcrCommands extends CommandsCli {
+
+       public JcrCommands(String commandName) {
+               super(commandName);
+               addCommand("sync", new JcrSync());
+       }
+
+       @Override
+       public String getDescription() {
+               return "Utilities around remote and local JCR repositories";
+       }
+
+}
diff --git a/org.argeo.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 (file)
index 0000000..401f447
--- /dev/null
@@ -0,0 +1,133 @@
+package org.argeo.cli.jcr;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.nio.file.Paths;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import javax.jcr.Credentials;
+import javax.jcr.Node;
+import javax.jcr.Repository;
+import javax.jcr.RepositoryException;
+import javax.jcr.RepositoryFactory;
+import javax.jcr.Session;
+import javax.jcr.SimpleCredentials;
+
+import org.apache.commons.cli.CommandLine;
+import org.apache.commons.cli.Option;
+import org.apache.commons.cli.Options;
+import org.apache.jackrabbit.core.RepositoryImpl;
+import org.apache.jackrabbit.core.config.RepositoryConfig;
+import org.argeo.cli.CommandArgsException;
+import org.argeo.cli.CommandRuntimeException;
+import org.argeo.cli.DescribedCommand;
+import org.argeo.jackrabbit.client.ClientDavexRepositoryFactory;
+import org.argeo.jcr.JcrUtils;
+import org.argeo.sync.SyncResult;
+
+public class JcrSync implements DescribedCommand<SyncResult<Node>> {
+       public final static String DEFAULT_LOCALFS_CONFIG = "repository-localfs.xml";
+
+       final static Option deleteOption = Option.builder().longOpt("delete").desc("delete from target").build();
+       final static Option recursiveOption = Option.builder("r").longOpt("recursive").desc("recurse into directories")
+                       .build();
+       final static Option progressOption = Option.builder().longOpt("progress").hasArg(false).desc("show progress")
+                       .build();
+
+       @Override
+       public SyncResult<Node> apply(List<String> t) {
+               try {
+                       CommandLine line = toCommandLine(t);
+                       List<String> remaining = line.getArgList();
+                       if (remaining.size() == 0) {
+                               throw new CommandArgsException("There must be at least one argument");
+                       }
+                       URI sourceUri = new URI(remaining.get(0));
+                       URI targetUri;
+                       if (remaining.size() == 1) {
+                               targetUri = Paths.get(System.getProperty("user.dir")).toUri();
+                       } else {
+                               targetUri = new URI(remaining.get(1));
+                       }
+                       boolean delete = line.hasOption(deleteOption.getLongOpt());
+                       boolean recursive = line.hasOption(recursiveOption.getLongOpt());
+
+                       // TODO make it configurable
+                       String sourceWorkspace = "home";
+                       String targetWorkspace = sourceWorkspace;
+
+                       final Repository sourceRepository;
+                       final Session sourceSession;
+                       Credentials sourceCredentials = null;
+                       final Repository targetRepository;
+                       final Session targetSession;
+                       Credentials targetCredentials = null;
+
+                       if ("http".equals(sourceUri.getScheme()) || "https".equals(sourceUri.getScheme())) {
+                               sourceRepository = createRemoteRepository(sourceUri);
+                       } else if (null == sourceUri.getScheme() || "file".equals(sourceUri.getScheme())) {
+                               RepositoryConfig repositoryConfig = RepositoryConfig.create(
+                                               JcrSync.class.getResourceAsStream(DEFAULT_LOCALFS_CONFIG), sourceUri.getPath().toString());
+                               sourceRepository = RepositoryImpl.create(repositoryConfig);
+                               sourceCredentials = new SimpleCredentials("admin", "admin".toCharArray());
+                       } else {
+                               throw new IllegalArgumentException("Unsupported scheme " + sourceUri.getScheme());
+                       }
+                       sourceSession = JcrUtils.loginOrCreateWorkspace(sourceRepository, sourceWorkspace, sourceCredentials);
+
+                       if ("http".equals(targetUri.getScheme()) || "https".equals(targetUri.getScheme())) {
+                               targetRepository = createRemoteRepository(targetUri);
+                       } else if (null == targetUri.getScheme() || "file".equals(targetUri.getScheme())) {
+                               RepositoryConfig repositoryConfig = RepositoryConfig.create(
+                                               JcrSync.class.getResourceAsStream(DEFAULT_LOCALFS_CONFIG), targetUri.getPath().toString());
+                               targetRepository = RepositoryImpl.create(repositoryConfig);
+                               targetCredentials = new SimpleCredentials("admin", "admin".toCharArray());
+                       } else {
+                               throw new IllegalArgumentException("Unsupported scheme " + targetUri.getScheme());
+                       }
+                       targetSession = JcrUtils.loginOrCreateWorkspace(targetRepository, targetWorkspace, targetCredentials);
+
+                       JcrUtils.copy(sourceSession.getRootNode(), targetSession.getRootNode());
+                       return new SyncResult<Node>();
+               } catch (URISyntaxException e) {
+                       throw new CommandArgsException(e);
+               } catch (Exception e) {
+                       throw new CommandRuntimeException(e, this, t);
+               }
+       }
+
+       protected Repository createRemoteRepository(URI uri) throws RepositoryException {
+               RepositoryFactory repositoryFactory = new ClientDavexRepositoryFactory();
+               Map<String, String> params = new HashMap<String, String>();
+               params.put(ClientDavexRepositoryFactory.JACKRABBIT_DAVEX_URI, uri.toString());
+               // FIXME make it configurable
+               params.put(ClientDavexRepositoryFactory.JACKRABBIT_REMOTE_DEFAULT_WORKSPACE, "sys");
+               return repositoryFactory.getRepository(params);
+       }
+
+       @Override
+       public Options getOptions() {
+               Options options = new Options();
+               options.addOption(recursiveOption);
+               options.addOption(deleteOption);
+               options.addOption(progressOption);
+               return options;
+       }
+
+       @Override
+       public String getUsage() {
+               return "[source URI] [target URI]";
+       }
+
+       public static void main(String[] args) {
+               DescribedCommand.mainImpl(new JcrSync(), args);
+       }
+
+       @Override
+       public String getDescription() {
+               return "Synchronises JCR repositories";
+       }
+
+}
diff --git a/org.argeo.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 (file)
index 0000000..6f3f01f
--- /dev/null
@@ -0,0 +1,2 @@
+/** JCR CLI commands. */
+package org.argeo.cli.jcr;
\ No newline at end of file
diff --git a/org.argeo.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 (file)
index 0000000..5e7759c
--- /dev/null
@@ -0,0 +1,76 @@
+<?xml version="1.0"?>
+<!--
+
+    Copyright (C) 2007-2012 Argeo GmbH
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+            http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+-->
+<!DOCTYPE Repository PUBLIC "-//The Apache Software Foundation//DTD Jackrabbit 1.6//EN"
+                            "http://jackrabbit.apache.org/dtd/repository-2.0.dtd">
+<Repository>
+       <!-- File system and datastore -->
+       <FileSystem class="org.apache.jackrabbit.core.fs.local.LocalFileSystem">
+               <param name="path" value="${rep.home}/repository" />
+       </FileSystem>
+       <DataStore class="org.apache.jackrabbit.core.data.FileDataStore">
+               <param name="path" value="${rep.home}/datastore" />
+       </DataStore>
+
+       <!-- Workspace templates -->
+       <Workspaces rootPath="${rep.home}/workspaces"
+               defaultWorkspace="main" configRootPath="/workspaces" />
+       <Workspace name="${wsp.name}">
+               <FileSystem class="org.apache.jackrabbit.core.fs.local.LocalFileSystem">
+                       <param name="path" value="${wsp.home}" />
+               </FileSystem>
+               <PersistenceManager
+                       class="org.apache.jackrabbit.core.persistence.bundle.BundleFsPersistenceManager">
+                       <param name="blobFSBlockSize" value="1" />
+               </PersistenceManager>
+               <SearchIndex class="org.apache.jackrabbit.core.query.lucene.SearchIndex">
+                       <param name="path" value="${rep.home}/repository/index" />
+               </SearchIndex>
+       </Workspace>
+
+       <!-- Versioning -->
+       <Versioning rootPath="${rep.home}/version">
+               <FileSystem class="org.apache.jackrabbit.core.fs.local.LocalFileSystem">
+                       <param name="path" value="${rep.home}/version" />
+               </FileSystem>
+               <PersistenceManager
+                       class="org.apache.jackrabbit.core.persistence.bundle.BundleFsPersistenceManager">
+                       <param name="blobFSBlockSize" value="1" />
+               </PersistenceManager>
+       </Versioning>
+
+       <!-- Indexing -->
+       <SearchIndex class="org.apache.jackrabbit.core.query.lucene.SearchIndex">
+               <param name="path" value="${rep.home}/repository/index" />
+               <param name="tikaConfigPath" value="tika-config.xml"/>
+       </SearchIndex>
+
+       <!-- Security -->
+       <Security appName="Jackrabbit">
+               <SecurityManager
+                       class="org.apache.jackrabbit.core.security.simple.SimpleSecurityManager"
+                       workspaceName="security" />
+               <AccessManager
+                       class="org.apache.jackrabbit.core.security.simple.SimpleAccessManager" />
+               <LoginModule
+                       class="org.apache.jackrabbit.core.security.simple.SimpleLoginModule">
+                       <param name="anonymousId" value="anonymous" />
+                       <param name="adminId" value="admin" />
+               </LoginModule>
+       </Security>
+</Repository>
\ No newline at end of file
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 (file)
index 0000000..7396c87
--- /dev/null
@@ -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<String, ?> sharedState, Map<String, ?> 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 (file)
index 0000000..9a49a06
--- /dev/null
@@ -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<JackrabbitDataModelMigration> {
+       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 (file)
index 0000000..77ad527
--- /dev/null
@@ -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 (file)
index 0000000..0f9db87
--- /dev/null
@@ -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 (file)
index 0000000..4b240f0
--- /dev/null
@@ -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 (file)
index 0000000..e08f4d6
--- /dev/null
@@ -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<String, String> params = new HashMap<String, String>();
+                       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 (file)
index 0000000..3fb0db9
--- /dev/null
@@ -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<HttpHost, AuthScheme> cache;
+
+       public NonSerialBasicAuthCache() {
+               cache = new ConcurrentHashMap<HttpHost, AuthScheme>();
+       }
+
+       @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 (file)
index 0000000..a2eb983
--- /dev/null
@@ -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 (file)
index 0000000..1cae6e4
--- /dev/null
@@ -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<String, JcrFileSystem> fileSystems = new HashMap<>();
+
+       @Override
+       public String getScheme() {
+               return "davex";
+       }
+
+       @Override
+       public FileSystem newFileSystem(URI uri, Map<String, ?> 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<String, String> params = new HashMap<String, String>();
+               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<String, Object>());
+                               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<Path> 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 (file)
index 0000000..e3a70d0
--- /dev/null
@@ -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<String, ?> 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<String, Object>());
+                       } 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 (file)
index 0000000..f2541fb
--- /dev/null
@@ -0,0 +1,57 @@
+<?xml version="1.0"?>
+<!DOCTYPE Repository PUBLIC "Jackrabbit 2.6" "http://jackrabbit.apache.org/dtd/repository-2.6.dtd">
+<Repository>
+       <!-- File system and datastore -->
+       <FileSystem
+               class="org.apache.jackrabbit.core.fs.mem.MemoryFileSystem" />
+
+       <!-- Workspace templates -->
+       <Workspaces rootPath="${rep.home}/workspaces"
+               defaultWorkspace="main" configRootPath="/workspaces" />
+       <Workspace name="${wsp.name}">
+               <FileSystem
+                       class="org.apache.jackrabbit.core.fs.mem.MemoryFileSystem" />
+               <PersistenceManager
+                       class="org.apache.jackrabbit.core.persistence.bundle.BundleFsPersistenceManager">
+               </PersistenceManager>
+               <SearchIndex
+                       class="org.apache.jackrabbit.core.query.lucene.SearchIndex">
+                       <param name="path" value="${wsp.home}/index" />
+                       <param name="directoryManagerClass"
+                               value="org.apache.jackrabbit.core.query.lucene.directory.RAMDirectoryManager" />
+                       <param name="extractorPoolSize" value="0" />
+                       <FileSystem
+                               class="org.apache.jackrabbit.core.fs.mem.MemoryFileSystem" />
+               </SearchIndex>
+       </Workspace>
+
+       <!-- Versioning -->
+       <Versioning rootPath="${rep.home}/version">
+               <FileSystem
+                       class="org.apache.jackrabbit.core.fs.mem.MemoryFileSystem" />
+               <PersistenceManager
+                       class="org.apache.jackrabbit.core.persistence.bundle.BundleFsPersistenceManager">
+               </PersistenceManager>
+       </Versioning>
+
+       <!-- Indexing -->
+       <SearchIndex
+               class="org.apache.jackrabbit.core.query.lucene.SearchIndex">
+               <param name="path" value="${rep.home}/index" />
+               <param name="directoryManagerClass"
+                       value="org.apache.jackrabbit.core.query.lucene.directory.RAMDirectoryManager" />
+               <param name="extractorPoolSize" value="0" />
+               <FileSystem
+                       class="org.apache.jackrabbit.core.fs.mem.MemoryFileSystem" />
+       </SearchIndex>
+
+       <!-- Security -->
+       <Security appName="Jackrabbit">
+               <LoginModule
+                       class="org.apache.jackrabbit.core.security.SimpleLoginModule" />
+               <!-- <SecurityManager class="org.argeo.security.jackrabbit.ArgeoSecurityManager" -->
+               <!-- workspaceName="security" /> -->
+               <!-- <AccessManager class="org.argeo.security.jackrabbit.ArgeoAccessManager" 
+                       /> -->
+       </Security>
+</Repository>
\ 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 (file)
index 0000000..c9ec2c3
--- /dev/null
@@ -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 (file)
index 0000000..17497d6
--- /dev/null
@@ -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 (file)
index 0000000..0526762
--- /dev/null
@@ -0,0 +1,82 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE Repository PUBLIC "Jackrabbit 2.6" "http://jackrabbit.apache.org/dtd/repository-2.6.dtd">
+<Repository>
+       <!-- Shared datasource -->
+       <DataSources>
+               <DataSource name="dataSource">
+                       <param name="driver" value="org.h2.Driver" />
+                       <param name="url" value="${dburl}" />
+                       <param name="user" value="${dbuser}" />
+                       <param name="password" value="${dbpassword}" />
+                       <param name="databaseType" value="h2" />
+                       <param name="maxPoolSize" value="${maxPoolSize}" />
+               </DataSource>
+       </DataSources>
+
+       <!-- File system and datastore -->
+       <FileSystem class="org.apache.jackrabbit.core.fs.db.DbFileSystem">
+               <param name="dataSourceName" value="dataSource" />
+               <param name="schema" value="default" />
+               <param name="schemaObjectPrefix" value="fs_" />
+       </FileSystem>
+       <DataStore class="org.apache.jackrabbit.core.data.FileDataStore">
+               <param name="path" value="${rep.home}/datastore" />
+       </DataStore>
+
+       <!-- Workspace templates -->
+       <Workspaces rootPath="${rep.home}/workspaces"
+               defaultWorkspace="${defaultWorkspace}" />
+       <Workspace name="${wsp.name}">
+               <FileSystem class="org.apache.jackrabbit.core.fs.db.DbFileSystem">
+                       <param name="dataSourceName" value="dataSource" />
+                       <param name="schema" value="default" />
+                       <param name="schemaObjectPrefix" value="${wsp.name}_fs_" />
+               </FileSystem>
+               <PersistenceManager
+                       class="org.apache.jackrabbit.core.persistence.pool.H2PersistenceManager">
+                       <param name="dataSourceName" value="dataSource" />
+                       <param name="schemaObjectPrefix" value="${wsp.name}_pm_" />
+                       <param name="bundleCacheSize" value="${bundleCacheMB}" />
+               </PersistenceManager>
+               <SearchIndex class="org.apache.jackrabbit.core.query.lucene.SearchIndex">
+                       <param name="path" value="${wsp.home}/index" />
+                       <param name="extractorPoolSize" value="${extractorPoolSize}" />
+                       <param name="cacheSize" value="${searchCacheSize}" />
+                       <param name="maxVolatileIndexSize" value="${maxVolatileIndexSize}" />
+               </SearchIndex>
+               <WorkspaceSecurity>
+                       <AccessControlProvider
+                               class="org.argeo.security.jackrabbit.ArgeoAccessControlProvider" />
+               </WorkspaceSecurity>
+       </Workspace>
+
+       <!-- Versioning -->
+       <Versioning rootPath="${rep.home}/version">
+               <FileSystem class="org.apache.jackrabbit.core.fs.db.DbFileSystem">
+                       <param name="dataSourceName" value="dataSource" />
+                       <param name="schema" value="default" />
+                       <param name="schemaObjectPrefix" value="fs_ver_" />
+               </FileSystem>
+               <PersistenceManager
+                       class="org.apache.jackrabbit.core.persistence.pool.H2PersistenceManager">
+                       <param name="dataSourceName" value="dataSource" />
+                       <param name="schemaObjectPrefix" value="pm_ver_" />
+                       <param name="bundleCacheSize" value="${bundleCacheMB}" />
+               </PersistenceManager>
+       </Versioning>
+
+       <!-- Indexing -->
+       <SearchIndex class="org.apache.jackrabbit.core.query.lucene.SearchIndex">
+               <param name="path" value="${rep.home}/index" />
+               <param name="extractorPoolSize" value="${extractorPoolSize}" />
+               <param name="cacheSize" value="${searchCacheSize}" />
+               <param name="maxVolatileIndexSize" value="${maxVolatileIndexSize}" />
+       </SearchIndex>
+
+       <!-- Security -->
+       <Security appName="Jackrabbit">
+               <SecurityManager class="org.argeo.security.jackrabbit.ArgeoSecurityManager"
+                       workspaceName="security" />
+               <AccessManager class="org.argeo.security.jackrabbit.ArgeoAccessManager" />
+       </Security>
+</Repository>
\ 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 (file)
index 0000000..3d24708
--- /dev/null
@@ -0,0 +1,60 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE Repository PUBLIC "Jackrabbit 2.6" "http://jackrabbit.apache.org/dtd/repository-2.6.dtd">
+<Repository>
+       <!-- File system and datastore -->
+       <FileSystem class="org.apache.jackrabbit.core.fs.local.LocalFileSystem">
+               <param name="path" value="${rep.home}/repository" />
+       </FileSystem>
+       <DataStore class="org.apache.jackrabbit.core.data.FileDataStore">
+               <param name="path" value="${rep.home}/datastore" />
+       </DataStore>
+
+       <!-- Workspace templates -->
+       <Workspaces rootPath="${rep.home}/workspaces"
+               defaultWorkspace="${defaultWorkspace}" />
+       <Workspace name="${wsp.name}">
+               <FileSystem class="org.apache.jackrabbit.core.fs.local.LocalFileSystem">
+                       <param name="path" value="${wsp.home}" />
+               </FileSystem>
+               <PersistenceManager
+                       class="org.apache.jackrabbit.core.persistence.bundle.BundleFsPersistenceManager">
+                       <param name="bundleCacheSize" value="${bundleCacheMB}" />
+               </PersistenceManager>
+               <SearchIndex class="org.apache.jackrabbit.core.query.lucene.SearchIndex">
+                       <param name="path" value="${wsp.home}/index" />
+                       <param name="extractorPoolSize" value="${extractorPoolSize}" />
+                       <param name="cacheSize" value="${searchCacheSize}" />
+                       <param name="maxVolatileIndexSize" value="${maxVolatileIndexSize}" />
+               </SearchIndex>
+               <WorkspaceSecurity>
+                       <AccessControlProvider
+                               class="org.argeo.security.jackrabbit.ArgeoAccessControlProvider" />
+               </WorkspaceSecurity>
+       </Workspace>
+
+       <!-- Versioning -->
+       <Versioning rootPath="${rep.home}/version">
+               <FileSystem class="org.apache.jackrabbit.core.fs.local.LocalFileSystem">
+                       <param name="path" value="${rep.home}/version" />
+               </FileSystem>
+               <PersistenceManager
+                       class="org.apache.jackrabbit.core.persistence.bundle.BundleFsPersistenceManager">
+                       <param name="bundleCacheSize" value="${bundleCacheMB}" />
+               </PersistenceManager>
+       </Versioning>
+
+       <!-- Indexing -->
+       <SearchIndex class="org.apache.jackrabbit.core.query.lucene.SearchIndex">
+               <param name="path" value="${rep.home}/index" />
+               <param name="extractorPoolSize" value="${extractorPoolSize}" />
+               <param name="cacheSize" value="${searchCacheSize}" />
+               <param name="maxVolatileIndexSize" value="${maxVolatileIndexSize}" />
+       </SearchIndex>
+
+       <!-- Security -->
+       <Security appName="Jackrabbit">
+               <SecurityManager class="org.argeo.security.jackrabbit.ArgeoSecurityManager"
+                       workspaceName="security" />
+               <AccessManager class="org.argeo.security.jackrabbit.ArgeoAccessManager" />
+       </Security>
+</Repository>
\ 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 (file)
index 0000000..ecee5bd
--- /dev/null
@@ -0,0 +1,55 @@
+<?xml version="1.0"?>
+<!DOCTYPE Repository PUBLIC "Jackrabbit 2.6" "http://jackrabbit.apache.org/dtd/repository-2.6.dtd">
+<Repository>
+       <!-- File system and datastore -->
+       <FileSystem class="org.apache.jackrabbit.core.fs.mem.MemoryFileSystem" />
+
+       <!-- Workspace templates -->
+       <Workspaces rootPath="${rep.home}/workspaces"
+               defaultWorkspace="${defaultWorkspace}" configRootPath="/workspaces" />
+       <Workspace name="${wsp.name}">
+               <FileSystem class="org.apache.jackrabbit.core.fs.mem.MemoryFileSystem" />
+               <PersistenceManager
+                       class="org.apache.jackrabbit.core.persistence.bundle.BundleFsPersistenceManager">
+                       <param name="blobFSBlockSize" value="1" />
+                       <param name="bundleCacheSize" value="${bundleCacheMB}" />
+               </PersistenceManager>
+               <SearchIndex class="org.apache.jackrabbit.core.query.lucene.SearchIndex">
+                       <param name="path" value="${wsp.home}/index" />
+                       <param name="directoryManagerClass"
+                               value="org.apache.jackrabbit.core.query.lucene.directory.RAMDirectoryManager" />
+                       <param name="extractorPoolSize" value="${extractorPoolSize}" />
+                       <param name="cacheSize" value="${searchCacheSize}" />
+                       <param name="maxVolatileIndexSize" value="${maxVolatileIndexSize}" />
+                       <FileSystem class="org.apache.jackrabbit.core.fs.mem.MemoryFileSystem" />
+               </SearchIndex>
+       </Workspace>
+
+       <!-- Versioning -->
+       <Versioning rootPath="${rep.home}/version">
+               <FileSystem class="org.apache.jackrabbit.core.fs.mem.MemoryFileSystem" />
+               <PersistenceManager
+                       class="org.apache.jackrabbit.core.persistence.bundle.BundleFsPersistenceManager">
+                       <param name="blobFSBlockSize" value="1" />
+                       <param name="bundleCacheSize" value="${bundleCacheMB}" />
+               </PersistenceManager>
+       </Versioning>
+
+       <!-- Indexing -->
+       <SearchIndex class="org.apache.jackrabbit.core.query.lucene.SearchIndex">
+               <param name="path" value="${rep.home}/index" />
+               <param name="directoryManagerClass"
+                       value="org.apache.jackrabbit.core.query.lucene.directory.RAMDirectoryManager" />
+               <param name="extractorPoolSize" value="${extractorPoolSize}" />
+               <param name="cacheSize" value="${searchCacheSize}" />
+               <param name="maxVolatileIndexSize" value="${maxVolatileIndexSize}" />
+               <FileSystem class="org.apache.jackrabbit.core.fs.mem.MemoryFileSystem" />
+       </SearchIndex>
+
+       <!-- Security -->
+       <Security appName="Jackrabbit">
+               <SecurityManager class="org.argeo.security.jackrabbit.ArgeoSecurityManager"
+                       workspaceName="security" />
+               <AccessManager class="org.argeo.security.jackrabbit.ArgeoAccessManager" />
+       </Security>
+</Repository>
\ 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 (file)
index 0000000..07a0d04
--- /dev/null
@@ -0,0 +1,82 @@
+<?xml version="1.0"?>
+<!DOCTYPE Repository PUBLIC "Jackrabbit 2.6" "http://jackrabbit.apache.org/dtd/repository-2.6.dtd">
+<Repository>
+       <!-- Shared datasource -->
+       <DataSources>
+               <DataSource name="dataSource">
+                       <param name="driver" value="org.postgresql.Driver" />
+                       <param name="url" value="${dburl}" />
+                       <param name="user" value="${dbuser}" />
+                       <param name="password" value="${dbpassword}" />
+                       <param name="databaseType" value="postgresql" />
+                       <param name="maxPoolSize" value="${maxPoolSize}" />
+               </DataSource>
+       </DataSources>
+
+       <!-- File system and datastore -->
+       <FileSystem class="org.apache.jackrabbit.core.fs.db.DbFileSystem">
+               <param name="dataSourceName" value="dataSource" />
+               <param name="schema" value="postgresql" />
+               <param name="schemaObjectPrefix" value="fs_" />
+       </FileSystem>
+       <DataStore class="org.apache.jackrabbit.core.data.FileDataStore">
+               <param name="path" value="${rep.home}/datastore" />
+       </DataStore>
+
+       <!-- Workspace templates -->
+       <Workspaces rootPath="${rep.home}/workspaces"
+               defaultWorkspace="${defaultWorkspace}" />
+       <Workspace name="${wsp.name}">
+               <FileSystem class="org.apache.jackrabbit.core.fs.db.DbFileSystem">
+                       <param name="dataSourceName" value="dataSource" />
+                       <param name="schema" value="postgresql" />
+                       <param name="schemaObjectPrefix" value="${wsp.name}_fs_" />
+               </FileSystem>
+               <PersistenceManager
+                       class="org.apache.jackrabbit.core.persistence.pool.PostgreSQLPersistenceManager">
+                       <param name="dataSourceName" value="dataSource" />
+                       <param name="schemaObjectPrefix" value="${wsp.name}_pm_" />
+                       <param name="bundleCacheSize" value="${bundleCacheMB}" />
+               </PersistenceManager>
+               <SearchIndex class="org.apache.jackrabbit.core.query.lucene.SearchIndex">
+                       <param name="path" value="${wsp.home}/index" />
+                       <param name="extractorPoolSize" value="${extractorPoolSize}" />
+                       <param name="cacheSize" value="${searchCacheSize}" />
+                       <param name="maxVolatileIndexSize" value="${maxVolatileIndexSize}" />
+               </SearchIndex>
+               <WorkspaceSecurity>
+                       <AccessControlProvider
+                               class="org.argeo.security.jackrabbit.ArgeoAccessControlProvider" />
+               </WorkspaceSecurity>
+       </Workspace>
+
+       <!-- Versioning -->
+       <Versioning rootPath="${rep.home}/version">
+               <FileSystem class="org.apache.jackrabbit.core.fs.db.DbFileSystem">
+                       <param name="dataSourceName" value="dataSource" />
+                       <param name="schema" value="postgresql" />
+                       <param name="schemaObjectPrefix" value="fs_ver_" />
+               </FileSystem>
+               <PersistenceManager
+                       class="org.apache.jackrabbit.core.persistence.pool.PostgreSQLPersistenceManager">
+                       <param name="dataSourceName" value="dataSource" />
+                       <param name="schemaObjectPrefix" value="pm_ver_" />
+                       <param name="bundleCacheSize" value="${bundleCacheMB}" />
+               </PersistenceManager>
+       </Versioning>
+
+       <!-- Indexing -->
+       <SearchIndex class="org.apache.jackrabbit.core.query.lucene.SearchIndex">
+               <param name="path" value="${rep.home}/index" />
+               <param name="extractorPoolSize" value="${extractorPoolSize}" />
+               <param name="cacheSize" value="${searchCacheSize}" />
+               <param name="maxVolatileIndexSize" value="${maxVolatileIndexSize}" />
+       </SearchIndex>
+
+       <!-- Security -->
+       <Security appName="Jackrabbit">
+               <SecurityManager class="org.argeo.security.jackrabbit.ArgeoSecurityManager"
+                       workspaceName="security" />
+               <AccessManager class="org.argeo.security.jackrabbit.ArgeoAccessManager" />
+       </Security>
+</Repository>
\ 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 (file)
index 0000000..9677828
--- /dev/null
@@ -0,0 +1,79 @@
+<?xml version="1.0"?>
+<!DOCTYPE Repository PUBLIC "Jackrabbit 2.6" "http://jackrabbit.apache.org/dtd/repository-2.6.dtd">
+<Repository>
+       <!-- Shared datasource -->
+       <DataSources>
+               <DataSource name="dataSource">
+                       <param name="driver" value="org.postgresql.Driver" />
+                       <param name="url" value="${dburl}" />
+                       <param name="user" value="${dbuser}" />
+                       <param name="password" value="${dbpassword}" />
+                       <param name="databaseType" value="postgresql" />
+                       <param name="maxPoolSize" value="${maxPoolSize}" />
+               </DataSource>
+       </DataSources>
+
+       <!-- File system and datastore -->
+       <FileSystem class="org.apache.jackrabbit.core.fs.db.DbFileSystem">
+               <param name="dataSourceName" value="dataSource" />
+               <param name="schema" value="postgresql" />
+               <param name="schemaObjectPrefix" value="fs_" />
+       </FileSystem>
+
+       <!-- Workspace templates -->
+       <Workspaces rootPath="${rep.home}/workspaces"
+               defaultWorkspace="${defaultWorkspace}" />
+       <Workspace name="${wsp.name}">
+               <FileSystem class="org.apache.jackrabbit.core.fs.db.DbFileSystem">
+                       <param name="dataSourceName" value="dataSource" />
+                       <param name="schema" value="postgresql" />
+                       <param name="schemaObjectPrefix" value="${wsp.name}_fs_" />
+               </FileSystem>
+               <PersistenceManager
+                       class="org.apache.jackrabbit.core.persistence.pool.PostgreSQLPersistenceManager">
+                       <param name="dataSourceName" value="dataSource" />
+                       <param name="schemaObjectPrefix" value="${wsp.name}_pm_" />
+                       <param name="bundleCacheSize" value="${bundleCacheMB}" />
+               </PersistenceManager>
+               <SearchIndex class="org.apache.jackrabbit.core.query.lucene.SearchIndex">
+                       <param name="path" value="${wsp.home}/index" />
+                       <param name="extractorPoolSize" value="${extractorPoolSize}" />
+                       <param name="cacheSize" value="${searchCacheSize}" />
+                       <param name="maxVolatileIndexSize" value="${maxVolatileIndexSize}" />
+               </SearchIndex>
+               <WorkspaceSecurity>
+                       <AccessControlProvider
+                               class="org.argeo.security.jackrabbit.ArgeoAccessControlProvider" />
+               </WorkspaceSecurity>
+       </Workspace>
+
+       <!-- Versioning -->
+       <Versioning rootPath="${rep.home}/version">
+               <FileSystem class="org.apache.jackrabbit.core.fs.db.DbFileSystem">
+                       <param name="dataSourceName" value="dataSource" />
+                       <param name="schema" value="postgresql" />
+                       <param name="schemaObjectPrefix" value="fs_ver_" />
+               </FileSystem>
+               <PersistenceManager
+                       class="org.apache.jackrabbit.core.persistence.pool.PostgreSQLPersistenceManager">
+                       <param name="dataSourceName" value="dataSource" />
+                       <param name="schemaObjectPrefix" value="pm_ver_" />
+                       <param name="bundleCacheSize" value="${bundleCacheMB}" />
+               </PersistenceManager>
+       </Versioning>
+
+       <!-- Indexing -->
+       <SearchIndex class="org.apache.jackrabbit.core.query.lucene.SearchIndex">
+               <param name="path" value="${rep.home}/index" />
+               <param name="extractorPoolSize" value="${extractorPoolSize}" />
+               <param name="cacheSize" value="${searchCacheSize}" />
+               <param name="maxVolatileIndexSize" value="${maxVolatileIndexSize}" />
+       </SearchIndex>
+
+       <!-- Security -->
+       <Security appName="Jackrabbit">
+               <SecurityManager class="org.argeo.security.jackrabbit.ArgeoSecurityManager"
+                       workspaceName="security" />
+               <AccessManager class="org.argeo.security.jackrabbit.ArgeoAccessManager" />
+       </Security>
+</Repository>
\ 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 (file)
index 0000000..a75c795
--- /dev/null
@@ -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<Privilege> privileges = new ArrayList<Privilege>();
+               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<Privilege> 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 (file)
index 0000000..f3a282c
--- /dev/null
@@ -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 (file)
index 0000000..f65432e
--- /dev/null
@@ -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 (file)
index 0000000..0313f91
--- /dev/null
@@ -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 (file)
index 0000000..3b6143b
--- /dev/null
@@ -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 (file)
index 0000000..348dc28
--- /dev/null
@@ -0,0 +1,81 @@
+<?xml version="1.0"?>
+<!DOCTYPE Repository PUBLIC "-//The Apache Software Foundation//DTD Jackrabbit 1.6//EN"
+                            "http://jackrabbit.apache.org/dtd/repository-2.0.dtd">
+<Repository>
+       <!-- Shared datasource -->
+       <DataSources>
+               <DataSource name="dataSource">
+                       <param name="driver" value="org.h2.Driver" />
+                       <param name="url" value="jdbc:h2:mem:jackrabbit" />
+                       <param name="user" value="sa" />
+                       <param name="password" value="" />
+                       <param name="databaseType" value="h2" />
+                       <param name="maxPoolSize" value="10" />
+               </DataSource>
+       </DataSources>
+
+       <!-- File system and datastore -->
+       <FileSystem class="org.apache.jackrabbit.core.fs.db.DbFileSystem">
+               <param name="dataSourceName" value="dataSource" />
+               <param name="schema" value="default" />
+               <param name="schemaObjectPrefix" value="fs_" />
+       </FileSystem>
+       <DataStore class="org.apache.jackrabbit.core.data.db.DbDataStore">
+               <param name="dataSourceName" value="dataSource" />
+               <param name="schemaObjectPrefix" value="ds_" />
+       </DataStore>
+
+       <!-- Workspace templates -->
+       <Workspaces rootPath="${rep.home}/workspaces"
+               defaultWorkspace="dev" />
+       <Workspace name="${wsp.name}">
+               <FileSystem class="org.apache.jackrabbit.core.fs.db.DbFileSystem">
+                       <param name="dataSourceName" value="dataSource" />
+                       <param name="schema" value="default" />
+                       <param name="schemaObjectPrefix" value="${wsp.name}_fs_" />
+               </FileSystem>
+               <PersistenceManager
+                       class="org.apache.jackrabbit.core.persistence.pool.H2PersistenceManager">
+                       <param name="dataSourceName" value="dataSource" />
+                       <param name="schemaObjectPrefix" value="${wsp.name}_pm_" />
+               </PersistenceManager>
+               <SearchIndex class="org.apache.jackrabbit.core.query.lucene.SearchIndex">
+                       <param name="path" value="${wsp.home}/index" />
+               </SearchIndex>
+       </Workspace>
+
+       <!-- Versioning -->
+       <Versioning rootPath="${rep.home}/version">
+               <FileSystem class="org.apache.jackrabbit.core.fs.db.DbFileSystem">
+                       <param name="dataSourceName" value="dataSource" />
+                       <param name="schema" value="default" />
+                       <param name="schemaObjectPrefix" value="fs_ver_" />
+               </FileSystem>
+               <PersistenceManager
+                       class="org.apache.jackrabbit.core.persistence.pool.H2PersistenceManager">
+                       <param name="dataSourceName" value="dataSource" />
+                       <param name="schemaObjectPrefix" value="pm_ver_" />
+               </PersistenceManager>
+       </Versioning>
+
+       <!-- Indexing -->
+       <SearchIndex class="org.apache.jackrabbit.core.query.lucene.SearchIndex">
+               <param name="path" value="${rep.home}/repository/index" />
+               <param name="extractorPoolSize" value="2" />
+               <param name="supportHighlighting" value="true" />
+       </SearchIndex>
+
+       <!-- Security -->
+       <Security appName="Jackrabbit">
+               <SecurityManager
+                       class="org.apache.jackrabbit.core.security.simple.SimpleSecurityManager"
+                       workspaceName="security" />
+               <AccessManager
+                       class="org.apache.jackrabbit.core.security.simple.SimpleAccessManager" />
+               <LoginModule
+                       class="org.apache.jackrabbit.core.security.simple.SimpleLoginModule">
+                       <param name="anonymousId" value="anonymous" />
+                       <param name="adminId" value="admin" />
+               </LoginModule>
+       </Security>
+</Repository>
\ No newline at end of file
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 (file)
index 0000000..8395424
--- /dev/null
@@ -0,0 +1,72 @@
+<?xml version="1.0"?>
+<!--
+
+    Copyright (C) 2007-2012 Argeo GmbH
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+            http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+-->
+<!DOCTYPE Repository PUBLIC "-//The Apache Software Foundation//DTD Jackrabbit 1.6//EN"
+                            "http://jackrabbit.apache.org/dtd/repository-2.0.dtd">
+<Repository>
+       <!-- File system and datastore -->
+       <FileSystem class="org.apache.jackrabbit.core.fs.mem.MemoryFileSystem" />
+
+       <!-- Workspace templates -->
+       <Workspaces rootPath="${rep.home}/workspaces"
+               defaultWorkspace="main" configRootPath="/workspaces" />
+       <Workspace name="${wsp.name}">
+               <FileSystem class="org.apache.jackrabbit.core.fs.mem.MemoryFileSystem" />
+               <PersistenceManager
+                       class="org.apache.jackrabbit.core.persistence.bundle.BundleFsPersistenceManager">
+                       <param name="blobFSBlockSize" value="1" />
+               </PersistenceManager>
+               <SearchIndex class="org.apache.jackrabbit.core.query.lucene.SearchIndex">
+                       <param name="path" value="${rep.home}/repository/index" />
+                       <param name="directoryManagerClass"
+                               value="org.apache.jackrabbit.core.query.lucene.directory.RAMDirectoryManager" />
+                       <FileSystem class="org.apache.jackrabbit.core.fs.mem.MemoryFileSystem" />
+               </SearchIndex>
+       </Workspace>
+
+       <!-- Versioning -->
+       <Versioning rootPath="${rep.home}/version">
+               <FileSystem class="org.apache.jackrabbit.core.fs.mem.MemoryFileSystem" />
+               <PersistenceManager
+                       class="org.apache.jackrabbit.core.persistence.bundle.BundleFsPersistenceManager">
+                       <param name="blobFSBlockSize" value="1" />
+               </PersistenceManager>
+       </Versioning>
+
+       <!-- Indexing -->
+       <SearchIndex class="org.apache.jackrabbit.core.query.lucene.SearchIndex">
+               <param name="path" value="${rep.home}/repository/index" />
+               <param name="directoryManagerClass"
+                       value="org.apache.jackrabbit.core.query.lucene.directory.RAMDirectoryManager" />
+               <FileSystem class="org.apache.jackrabbit.core.fs.mem.MemoryFileSystem" />
+       </SearchIndex>
+
+       <!-- Security -->
+       <Security appName="Jackrabbit">
+               <SecurityManager
+                       class="org.apache.jackrabbit.core.security.simple.SimpleSecurityManager"
+                       workspaceName="security" />
+               <AccessManager
+                       class="org.apache.jackrabbit.core.security.simple.SimpleAccessManager" />
+               <LoginModule
+                       class="org.apache.jackrabbit.core.security.simple.SimpleLoginModule">
+                       <param name="anonymousId" value="anonymous" />
+                       <param name="adminId" value="admin" />
+               </LoginModule>
+       </Security>
+</Repository>
\ No newline at end of file
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 (file)
index 0000000..0418810
--- /dev/null
@@ -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 (file)
index 0000000..b4124ee
--- /dev/null
@@ -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<Node> iterator;
+       private Integer position = 0;
+
+       public CollectionNodeIterator(Collection<Node> 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 (file)
index 0000000..fc68888
--- /dev/null
@@ -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 (file)
index 0000000..72e325d
--- /dev/null
@@ -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 <code>null</code>
+ * 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 <code>Files</code> 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";
+       /**
+        * <code>jcr:name</code>, 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";
+       /**
+        * <code>jcr:path</code>, 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";
+       /**
+        * <code>jcr:primaryType</code> with prefix instead of namespace (as in
+        * {@link Property#JCR_PRIMARY_TYPE}.
+        */
+       public final static String JCR_PRIMARY_TYPE = "jcr:primaryType";
+       /**
+        * <code>jcr:mixinTypes</code> with prefix instead of namespace (as in
+        * {@link Property#JCR_MIXIN_TYPES}.
+        */
+       public final static String JCR_MIXIN_TYPES = "jcr:mixinTypes";
+       /**
+        * <code>jcr:uuid</code> with prefix instead of namespace (as in
+        * {@link Property#JCR_UUID}.
+        */
+       public final static String JCR_UUID = "jcr:uuid";
+       /**
+        * <code>jcr:created</code> with prefix instead of namespace (as in
+        * {@link Property#JCR_CREATED}.
+        */
+       public final static String JCR_CREATED = "jcr:created";
+       /**
+        * <code>jcr:createdBy</code> with prefix instead of namespace (as in
+        * {@link Property#JCR_CREATED_BY}.
+        */
+       public final static String JCR_CREATED_BY = "jcr:createdBy";
+       /**
+        * <code>jcr:lastModified</code> with prefix instead of namespace (as in
+        * {@link Property#JCR_LAST_MODIFIED}.
+        */
+       public final static String JCR_LAST_MODIFIED = "jcr:lastModified";
+       /**
+        * <code>jcr:lastModifiedBy</code> 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<Node> iterate(NodeIterator nodeIterator) {
+               return new Iterable<Node>() {
+
+                       @Override
+                       public Iterator<Node> 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<Node> 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<Node> getNodes(Node node) {
+               List<Node> 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 <code>null</node> 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 <code>null</node> 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 <code>null</code>, the primary
+        *                    type will be used (typically for XML structures)
+        * @param primaryType the primary type, if <code>null</code>
+        *                    {@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 <code>null</code>
+        * @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 <code>null</node> 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
+        * <code>null</code>.
+        * 
+        * @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
+        *         <code>null</code> 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
+        *         <code>defaultValue</code> 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 <code>null</code> 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
+        *         <code>defaultValue</code> 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> 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 <T> List<T> 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 <T> List<T> getMultiple(Property p) {
+               try {
+                       List<T> 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<Version> getLinearVersions(VersionHistory versionHistory) {
+               try {
+                       List<Version> 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 <code>null</code> 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 <code>null</code> 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 (file)
index 0000000..351929f
--- /dev/null
@@ -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<br/>
+        * value := group1,group2,user1
+        */
+       private Map<String, String> principalPrivileges = new HashMap<String, String>();
+
+       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<Privilege> privs = new ArrayList<Privilege>();
+                       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<Privilege> 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<String, String> groupPrivileges) {
+               this.principalPrivileges = groupPrivileges;
+       }
+
+       public void setPrincipalPrivileges(Map<String, String> 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 (file)
index 0000000..efbaabe
--- /dev/null
@@ -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<Session, Object> {
+       /** @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 (file)
index 0000000..c778743
--- /dev/null
@@ -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 (file)
index 0000000..71cf961
--- /dev/null
@@ -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 <code>UNKNOWN</code> 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 <code>true</code> if cancellation has been requested, and
+        *         <code>false</code> otherwise
+        * @see #setCanceled(boolean)
+        */
+       public boolean isCanceled();
+
+       /**
+        * Sets the cancel state to the given value.
+        * 
+        * @param value
+        *            <code>true</code> indicates that cancelation has been
+        *            requested (but not necessarily acknowledged);
+        *            <code>false</code> 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 (file)
index 0000000..3228eee
--- /dev/null
@@ -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<String, String> 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<String> 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 (file)
index 0000000..82a65e7
--- /dev/null
@@ -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 (file)
index 0000000..3be8be1
--- /dev/null
@@ -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<String> 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<Node> nodeIteratorToList(NodeIterator nodeIterator) {
+               List<Node> nodes = new ArrayList<Node>();
+               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<String> 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<String> 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<String> tokenize(String path) {
+               List<String> tokens = new ArrayList<String>();
+               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<String, PropertyDiff> diffProperties(Node reference, Node observed) {
+               Map<String, PropertyDiff> diffs = new TreeMap<String, PropertyDiff>();
+               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<String, PropertyDiff> 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<String, PropertyDiff> diffProperties(Node reference, Node observed, List<String> properties) {
+               Map<String, PropertyDiff> diffs = new TreeMap<String, PropertyDiff>();
+               try {
+                       Iterator<String> 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 <code>false</code>, 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,
+        * <a href="https://issues.apache.org/jira/browse/JCR-2233">these properties are
+        * not automatically updated</a>, 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
+        * <a href="http://jackrabbit.apache.org/node-type-notation.html"> Node type
+        * Notation </a> 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<Privilege> privileges = new ArrayList<Privilege>();
+               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<Privilege> 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("</");
+                       sb.append(nodeName);
+                       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 (file)
index 0000000..666b259
--- /dev/null
@@ -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<Value> 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<String> 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<String> getChecksums(Node node) {
+               try {
+                       List<String> 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 (file)
index 0000000..9dd43ad
--- /dev/null
@@ -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 (file)
index 0000000..0cbad33
--- /dev/null
@@ -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 (file)
index 0000000..71e76fe
--- /dev/null
@@ -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 (file)
index 0000000..4f42f2d
--- /dev/null
@@ -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 (file)
index 0000000..1e23338
--- /dev/null
@@ -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<Repository> repositories;
+
+       private ThreadLocal<Session> session = new ThreadLocal<Session>();
+       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<Thread> threads = Collections.synchronizedList(new ArrayList<Thread>());
+       private final Map<Long, Session> activeSessions = Collections.synchronizedMap(new HashMap<Long, Session>());
+       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<Thread> 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<? extends Session> 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<Repository> 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<Repository> 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 (file)
index 0000000..dab5554
--- /dev/null
@@ -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<String, PropertyDiff> diffs;
+       private Calendar updateTime;
+
+       public VersionDiff(String userId, Calendar updateTime,
+                       Map<String, PropertyDiff> diffs) {
+               this.userId = userId;
+               this.updateTime = updateTime;
+               this.diffs = diffs;
+       }
+
+       public String getUserId() {
+               return userId;
+       }
+
+       public Map<String, PropertyDiff> 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 (file)
index 0000000..d6550fe
--- /dev/null
@@ -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 (file)
index 0000000..7c9711b
--- /dev/null
@@ -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 (file)
index 0000000..3d538e8
--- /dev/null
@@ -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<String, WorkspaceFileStore> 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<JcrPath> listDirectMounts(Path base) {
+               String baseStr = base.toString();
+               Set<JcrPath> 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<Path> getRootDirectories() {
+               Set<Path> single = new HashSet<>();
+               single.add(new JcrPath(this, JcrPath.separator));
+               return single;
+       }
+
+       @Override
+       public Iterable<FileStore> getFileStores() {
+               List<FileStore> stores = new ArrayList<>();
+               stores.add(baseFileStore);
+               stores.addAll(mounts.values());
+               return stores;
+       }
+
+       @Override
+       public Set<String> supportedFileAttributeViews() {
+               try {
+                       String[] prefixes = session.getNamespacePrefixes();
+                       Set<String> 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 (file)
index 0000000..74d9a19
--- /dev/null
@@ -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<? extends OpenOption> 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<Path> newDirectoryStream(Path dir, Filter<? super Path> 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 extends FileAttributeView> V getFileAttributeView(Path path, Class<V> type, LinkOption... options) {
+               throw new UnsupportedOperationException();
+       }
+
+       @SuppressWarnings("unchecked")
+       @Override
+       public <A extends BasicFileAttributes> A readAttributes(Path path, Class<A> 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<String, Object> readAttributes(Path path, String attributes, LinkOption... options) throws IOException {
+               try {
+                       Node node = toNode(path);
+                       String pattern = attributes.replace(',', '|');
+                       Map<String, Object> res = new HashMap<String, Object>();
+                       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 (file)
index 0000000..f214fdc
--- /dev/null
@@ -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 (file)
index 0000000..1a4d747
--- /dev/null
@@ -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<Path> iterator() {
+               return new Iterator<Path>() {
+                       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 (file)
index 0000000..eda07a5
--- /dev/null
@@ -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<Path> {
+       private final JcrFileSystem fs;
+       private final NodeIterator nodeIterator;
+       private final Iterator<JcrPath> additionalPaths;
+       private final Filter<? super Path> filter;
+
+       public NodeDirectoryStream(JcrFileSystem fs, NodeIterator nodeIterator, Iterator<JcrPath> additionalPaths,
+                       Filter<? super Path> filter) {
+               this.fs = fs;
+               this.nodeIterator = nodeIterator;
+               this.additionalPaths = additionalPaths;
+               this.filter = filter;
+       }
+
+       @Override
+       public void close() throws IOException {
+       }
+
+       @Override
+       public Iterator<Path> iterator() {
+               return new Iterator<Path>() {
+                       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 (file)
index 0000000..8054d52
--- /dev/null
@@ -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 (file)
index 0000000..4643c8c
--- /dev/null
@@ -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;
+
+/**
+ * <b>Hacked from org.apache.jackrabbit.util.Text in Jackrabbit JCR Commons</b>
+ * 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 <code>true</code>, 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<String> strings = new ArrayList<String>();
+               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 <code>oldString</code> in <code>text</code>
+        * with <code>newString</code>.
+        *
+        * @param text
+        * @param oldString
+        *            old substring to be replaced with <code>newString</code>
+        * @param newString
+        *            new substring to replace occurrences of <code>oldString</code>
+        * @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("&lt;");
+                       } else if (ch == '>') {
+                               buf.append("&gt;");
+                       } else if (ch == '&') {
+                               buf.append("&amp;");
+                       } else if (ch == '"') {
+                               buf.append("&quot;");
+                       } else if (ch == '\'') {
+                               buf.append(isHtml ? "&#39;" : "&apos;");
+                       }
+               }
+               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 <code>escape()</code>
+        * and <code>unescape()</code> METHODS. They contains the characters as
+        * defined 'unreserved' in section 2.3 of the RFC 2396 'URI generic syntax':
+        * <p>
+        * 
+        * <pre>
+        * unreserved  = alphanum | mark
+        * mark        = "-" | "_" | "." | "!" | "~" | "*" | "'" | "(" | ")"
+        * </pre>
+        */
+       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 <code>string</code> using the
+        * <code>escape</code> 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 <code>string</code> is <code>null</code>.
+        */
+       public static String escape(String string, char escape) {
+               return escape(string, escape, false);
+       }
+
+       /**
+        * Does an URL encoding of the <code>string</code> using the
+        * <code>escape</code> 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 <code>isPath</code> is
+        * <code>true</code>, additionally the slash '/' is ignored, too.
+        *
+        * @param string
+        *            the string to encode.
+        * @param escape
+        *            the escape character.
+        * @param isPath
+        *            if <code>true</code>, the string is treated as path
+        * @return the escaped string
+        * @throws NullPointerException
+        *             if <code>string</code> is <code>null</code>.
+        */
+       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 <code>string</code>. 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 <code>string</code> is <code>null</code>.
+        */
+       public static String escape(String string) {
+               return escape(string, '%');
+       }
+
+       /**
+        * Does a URL encoding of the <code>path</code>. 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 <code>path</code> is <code>null</code>.
+        */
+       public static String escapePath(String path) {
+               return escape(path, '%', true);
+       }
+
+       /**
+        * Does a URL decoding of the <code>string</code> using the
+        * <code>escape</code> 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 <code>string</code> is <code>null</code>.
+        * @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 <code>string</code>. 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 <code>string</code> is <code>null</code>.
+        * @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.
+        * <p>
+        * QName EBNF:<br>
+        * <xmp> 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 *) </xmp>
+        *
+        * @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.
+        * <p>
+        * QName EBNF:<br>
+        * <xmp> 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 *) </xmp>
+        *
+        * @since Apache Jackrabbit 2.3.2 and 2.2.10
+        * @see <a href=
+        *      "https://issues.apache.org/jira/browse/JCR-3128">JCR-3128</a>
+        * @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.
+        * <p>
+        * Example:<br>
+        * 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.
+        * <p>
+        * 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 <code>null</code> if <code>path</code> is
+        *         <code>null</code>.
+        */
+       public static String getName(String path) {
+               return getName(path, '/');
+       }
+
+       /**
+        * Returns the name part of the path, delimited by the given
+        * <code>delim</code>. If the given path is already a name (i.e. contains no
+        * <code>delim</code> characters) it is returned.
+        *
+        * @param path
+        *            the path
+        * @param delim
+        *            the delimiter
+        * @return the name part or <code>null</code> if <code>path</code> is
+        *         <code>null</code>.
+        */
+       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 <code>qname</code>. If the
+        * prefix is missing, an empty string is returned. Please note, that this
+        * method does not validate the name or prefix.
+        * </p>
+        * 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 <code>qname</code> is <code>null</code>
+        */
+       public static String getNamespacePrefix(String qname) {
+               int pos = qname.indexOf(':');
+               return pos >= 0 ? qname.substring(0, pos) : "";
+       }
+
+       /**
+        * Returns the local name of the given <code>qname</code>. Please note, that
+        * this method does not validate the name.
+        * </p>
+        * the qname has the format: qname := [prefix ':'] local;
+        *
+        * @param qname
+        *            a qualified name
+        * @return the localname
+        *
+        * @see #getNamespacePrefix(String)
+        *
+        * @throws NullPointerException
+        *             if <code>qname</code> is <code>null</code>
+        */
+       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 <code>descendant</code> path is hierarchical a
+        * descendant of <code>path</code>.
+        *
+        * @param path
+        *            the current path
+        * @param descendant
+        *            the potential descendant
+        * @return <code>true</code> if the <code>descendant</code> is a descendant;
+        *         <code>false</code> 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 <code>descendant</code> path is hierarchical a
+        * descendant of <code>path</code> or equal to it.
+        *
+        * @param path
+        *            the path to check
+        * @param descendant
+        *            the potential descendant
+        * @return <code>true</code> if the <code>descendant</code> is a descendant
+        *         or equal; <code>false</code> 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 n<sup>th</sup> relative parent of the path, where n=level.
+        * <p>
+        * Example:<br>
+        * <code>
+        * Text.getRelativeParent("/foo/bar/test", 1) == "/foo/bar"
+        * </code>
+        *
+        * @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 n<sup>th</sup> absolute parent of the path, where n=level.
+        * <p>
+        * Example:<br>
+        * <code>
+        * Text.getAbsoluteParent("/foo/bar/test", 1) == "/foo/bar"
+        * </code>
+        *
+        * @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
+        * <code>${...}</code> 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
+        * <code>ignoreMissing</code> is <code>true</code>. In the later case, the
+        * missing variable is replaced by the empty string.
+        *
+        * @param value
+        *            the original value
+        * @param ignoreMissing
+        *            if <code>true</code>, 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 (file)
index 0000000..6d9d05c
--- /dev/null
@@ -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<? extends FileAttributeView> type) {
+               return false;
+       }
+
+       @Override
+       public boolean supportsFileAttributeView(String name) {
+               return false;
+       }
+
+       @Override
+       public <V extends FileStoreAttributeView> V getFileStoreAttributeView(Class<V> 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 (file)
index 0000000..0cdfdaf
--- /dev/null
@@ -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 (file)
index 0000000..3eb0e7a
--- /dev/null
@@ -0,0 +1,16 @@
+//
+// JCR EXTENSIONS
+//
+<jcrx = "http://www.argeo.org/ns/jcrx">
+
+[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 (file)
index 0000000..1837749
--- /dev/null
@@ -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 (file)
index 0000000..0984276
--- /dev/null
@@ -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 (file)
index 0000000..84eea1f
--- /dev/null
@@ -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, <code>null</code> 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 (file)
index 0000000..d77bd49
--- /dev/null
@@ -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 (file)
index 0000000..a578c45
--- /dev/null
@@ -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 (file)
index 0000000..dc2963a
--- /dev/null
@@ -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<Session>() {
+
+                               @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 (file)
index 0000000..c6e7415
--- /dev/null
@@ -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 (file)
index 0000000..2adb6a9
--- /dev/null
@@ -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
+        * <code>false</code>.
+        */
+       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 <code>null</code> 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
+        *                             <code>false</code>, 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<String, String> 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<String, String> 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("</");
+               writer.append(tagName);
+               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 (file)
index 0000000..813d065
--- /dev/null
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
+  <xsl:output method="xml" indent="yes"/>
+  <xsl:template match="/|comment()|processing-instruction()">
+    <xsl:copy>
+      <xsl:apply-templates/>
+    </xsl:copy>
+  </xsl:template>
+  <xsl:template match="*">
+    <xsl:element name="{local-name()}">
+      <xsl:apply-templates select="@*|node()"/>
+    </xsl:element>
+  </xsl:template>
+  <xsl:template match="@*">
+    <xsl:attribute name="{local-name()}">
+      <xsl:value-of select="."/>
+    </xsl:attribute>
+  </xsl:template>
+</xsl:stylesheet>
\ 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 (file)
index 0000000..6003d63
--- /dev/null
@@ -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<String> 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<String> 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<String> getRequiredRoles() {
+               return new HashSet<>();
+       }
+
+       public void destroy() {
+
+       }
+
+       /*
+        * UTILITIES
+        */
+
+       /** Create these roles as group if they don't exist. */
+       protected void makeSureRolesExists(EnumSet<? extends Distinguished> enumSet) {
+               makeSureRolesExists(Distinguished.enumToDns(enumSet));
+       }
+
+       /** Create these roles as group if they don't exist. */
+       protected void makeSureRolesExists(Set<String> 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 (file)
index 0000000..a30fe97
--- /dev/null
@@ -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<String> roles = new ArrayList<String>();
+       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<String> 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 (file)
index 0000000..ef83c1f
--- /dev/null
@@ -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<String> 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("<!-- ");
+                                               out.write(getCurrentJcrPath());
+                                               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("</");
+                               out.write(SV_PREFIX + ":" + localName);
+                               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<String> 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 (file)
index 0000000..60e8f8e
--- /dev/null
@@ -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<String, String> 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<Set<String>> 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<Set<String>> contentPathsFuture = executorService
+                                               .submit(() -> performNodeBackup(defaultWorkspace, nodePath));
+                               executorService.submit(() -> performFilesBackup(defaultWorkspace, contentPathsFuture));
+                       }
+               } finally {
+                       Jcr.logout(session);
+               }
+
+       }
+
+       protected Set<String> 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<String> 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<Set<String>> contentPathsFuture) {
+               Set<String> 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 <remote URL> [<target directory>]");
+
+       }
+
+       protected static Repository createRemoteRepository(URI uri) throws RepositoryException {
+               RepositoryFactory repositoryFactory = new ClientDavexRepositoryFactory();
+               Map<String, String> params = new HashMap<String, String>();
+               params.put(ClientDavexRepositoryFactory.JACKRABBIT_DAVEX_URI, uri.toString());
+               // 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<String, String> headers = bundle.getHeaders();
+                       Manifest manifest = new Manifest();
+                       Enumeration<String> 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<URL> 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 (file)
index 0000000..a12bb41
--- /dev/null
@@ -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<Path> 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<Path> workspaceDirs = Files.newDirectoryStream(workspaces)) {
+                               for (Path workspacePath : workspaceDirs) {
+                                       String workspaceName = workspacePath.getFileName().toString();
+                                       Session session = JcrUtils.loginOrCreateWorkspace(repository, workspaceName);
+                                       try (DirectoryStream<Path> 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 (file)
index 0000000..a61e19b
--- /dev/null
@@ -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 (file)
index 0000000..ef40ab3
--- /dev/null
@@ -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 (file)
index 0000000..1ce974c
--- /dev/null
@@ -0,0 +1,2 @@
+/** Utilities for the maintenance of an Argeo Node. */
+package org.argeo.maintenance;
\ No newline at end of file
index e19f92e01ea955b1e6bd3643cff5a18294e4553d..0a313033b1d395c2654f13d6ea41ee98a04fdf70 100644 (file)
@@ -18,7 +18,7 @@
                </dependency>
                <dependency>
                        <groupId>org.argeo.commons</groupId>
-                       <artifactId>org.argeo.jcr</artifactId>
+                       <artifactId>org.argeo.cms.jcr</artifactId>
                        <version>2.3-SNAPSHOT</version>
                </dependency>
                <dependency>
                        <artifactId>org.argeo.core</artifactId>
                        <version>2.3-SNAPSHOT</version>
                </dependency>
-               <dependency>
-                       <groupId>org.argeo.commons</groupId>
-                       <artifactId>org.argeo.maintenance</artifactId>
-                       <version>2.3-SNAPSHOT</version>
-               </dependency>
+<!--           <dependency> -->
+<!--                   <groupId>org.argeo.commons</groupId> -->
+<!--                   <artifactId>org.argeo.maintenance</artifactId> -->
+<!--                   <version>2.3-SNAPSHOT</version> -->
+<!--           </dependency> -->
        </dependencies>
 </project>
\ 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 (file)
index 7e2e119..0000000
+++ /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
index f096e4f29aff6627162d4a30b8e1152cd987f094..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 100644 (file)
@@ -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
index 49a93ba4a0bf7ecffb4898671d84c604be9105fc..353d0422a96a0f275349cbc9c552c0d203b6151e 100644 (file)
@@ -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 (file)
index 2d03b8f..0000000
+++ /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<Path> 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<Path> 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<String, Object> 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<Path> 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());
-       }
-}
index 37d89574d4b423eaf9e0d19c5bd0343b7c71910b..ad5afd468f6d4954da547562befa87ec18d37e79 100644 (file)
                        <artifactId>org.argeo.enterprise</artifactId>
                        <version>2.3-SNAPSHOT</version>
                </dependency>
-               <dependency>
-                       <groupId>org.argeo.commons</groupId>
-                       <artifactId>org.argeo.jcr</artifactId>
-                       <version>2.3-SNAPSHOT</version>
-               </dependency>
+<!--           <dependency> -->
+<!--                   <groupId>org.argeo.commons</groupId> -->
+<!--                   <artifactId>org.argeo.jcr</artifactId> -->
+<!--                   <version>2.3-SNAPSHOT</version> -->
+<!--           </dependency> -->
        </dependencies>
 </project>
\ No newline at end of file
index 902318c44e01d958b85e203d0c8893f0965f6d09..9ab9cafad72db38d14022f0f352def5f4c314dfb 100644 (file)
@@ -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<SyncResult<Path>> {
                        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 (file)
index ea74674..0000000
+++ /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 (file)
index 401f447..0000000
+++ /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<SyncResult<Node>> {
-       public final static String DEFAULT_LOCALFS_CONFIG = "repository-localfs.xml";
-
-       final static Option deleteOption = Option.builder().longOpt("delete").desc("delete from target").build();
-       final static Option recursiveOption = Option.builder("r").longOpt("recursive").desc("recurse into directories")
-                       .build();
-       final static Option progressOption = Option.builder().longOpt("progress").hasArg(false).desc("show progress")
-                       .build();
-
-       @Override
-       public SyncResult<Node> apply(List<String> t) {
-               try {
-                       CommandLine line = toCommandLine(t);
-                       List<String> remaining = line.getArgList();
-                       if (remaining.size() == 0) {
-                               throw new CommandArgsException("There must be at least one argument");
-                       }
-                       URI sourceUri = new URI(remaining.get(0));
-                       URI targetUri;
-                       if (remaining.size() == 1) {
-                               targetUri = Paths.get(System.getProperty("user.dir")).toUri();
-                       } else {
-                               targetUri = new URI(remaining.get(1));
-                       }
-                       boolean delete = line.hasOption(deleteOption.getLongOpt());
-                       boolean recursive = line.hasOption(recursiveOption.getLongOpt());
-
-                       // TODO make it configurable
-                       String sourceWorkspace = "home";
-                       String targetWorkspace = sourceWorkspace;
-
-                       final Repository sourceRepository;
-                       final Session sourceSession;
-                       Credentials sourceCredentials = null;
-                       final Repository targetRepository;
-                       final Session targetSession;
-                       Credentials targetCredentials = null;
-
-                       if ("http".equals(sourceUri.getScheme()) || "https".equals(sourceUri.getScheme())) {
-                               sourceRepository = createRemoteRepository(sourceUri);
-                       } else if (null == sourceUri.getScheme() || "file".equals(sourceUri.getScheme())) {
-                               RepositoryConfig repositoryConfig = RepositoryConfig.create(
-                                               JcrSync.class.getResourceAsStream(DEFAULT_LOCALFS_CONFIG), sourceUri.getPath().toString());
-                               sourceRepository = RepositoryImpl.create(repositoryConfig);
-                               sourceCredentials = new SimpleCredentials("admin", "admin".toCharArray());
-                       } else {
-                               throw new IllegalArgumentException("Unsupported scheme " + sourceUri.getScheme());
-                       }
-                       sourceSession = JcrUtils.loginOrCreateWorkspace(sourceRepository, sourceWorkspace, sourceCredentials);
-
-                       if ("http".equals(targetUri.getScheme()) || "https".equals(targetUri.getScheme())) {
-                               targetRepository = createRemoteRepository(targetUri);
-                       } else if (null == targetUri.getScheme() || "file".equals(targetUri.getScheme())) {
-                               RepositoryConfig repositoryConfig = RepositoryConfig.create(
-                                               JcrSync.class.getResourceAsStream(DEFAULT_LOCALFS_CONFIG), targetUri.getPath().toString());
-                               targetRepository = RepositoryImpl.create(repositoryConfig);
-                               targetCredentials = new SimpleCredentials("admin", "admin".toCharArray());
-                       } else {
-                               throw new IllegalArgumentException("Unsupported scheme " + targetUri.getScheme());
-                       }
-                       targetSession = JcrUtils.loginOrCreateWorkspace(targetRepository, targetWorkspace, targetCredentials);
-
-                       JcrUtils.copy(sourceSession.getRootNode(), targetSession.getRootNode());
-                       return new SyncResult<Node>();
-               } catch (URISyntaxException e) {
-                       throw new CommandArgsException(e);
-               } catch (Exception e) {
-                       throw new CommandRuntimeException(e, this, t);
-               }
-       }
-
-       protected Repository createRemoteRepository(URI uri) throws RepositoryException {
-               RepositoryFactory repositoryFactory = new ClientDavexRepositoryFactory();
-               Map<String, String> params = new HashMap<String, String>();
-               params.put(ClientDavexRepositoryFactory.JACKRABBIT_DAVEX_URI, uri.toString());
-               // FIXME make it configurable
-               params.put(ClientDavexRepositoryFactory.JACKRABBIT_REMOTE_DEFAULT_WORKSPACE, "sys");
-               return repositoryFactory.getRepository(params);
-       }
-
-       @Override
-       public Options getOptions() {
-               Options options = new Options();
-               options.addOption(recursiveOption);
-               options.addOption(deleteOption);
-               options.addOption(progressOption);
-               return options;
-       }
-
-       @Override
-       public String getUsage() {
-               return "[source URI] [target URI]";
-       }
-
-       public static void main(String[] args) {
-               DescribedCommand.mainImpl(new JcrSync(), args);
-       }
-
-       @Override
-       public String getDescription() {
-               return "Synchronises JCR repositories";
-       }
-
-}
diff --git a/org.argeo.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 (file)
index 6f3f01f..0000000
+++ /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 (file)
index 5e7759c..0000000
+++ /dev/null
@@ -1,76 +0,0 @@
-<?xml version="1.0"?>
-<!--
-
-    Copyright (C) 2007-2012 Argeo GmbH
-
-    Licensed under the Apache License, Version 2.0 (the "License");
-    you may not use this file except in compliance with the License.
-    You may obtain a copy of the License at
-
-            http://www.apache.org/licenses/LICENSE-2.0
-
-    Unless required by applicable law or agreed to in writing, software
-    distributed under the License is distributed on an "AS IS" BASIS,
-    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-    See the License for the specific language governing permissions and
-    limitations under the License.
-
--->
-<!DOCTYPE Repository PUBLIC "-//The Apache Software Foundation//DTD Jackrabbit 1.6//EN"
-                            "http://jackrabbit.apache.org/dtd/repository-2.0.dtd">
-<Repository>
-       <!-- File system and datastore -->
-       <FileSystem class="org.apache.jackrabbit.core.fs.local.LocalFileSystem">
-               <param name="path" value="${rep.home}/repository" />
-       </FileSystem>
-       <DataStore class="org.apache.jackrabbit.core.data.FileDataStore">
-               <param name="path" value="${rep.home}/datastore" />
-       </DataStore>
-
-       <!-- Workspace templates -->
-       <Workspaces rootPath="${rep.home}/workspaces"
-               defaultWorkspace="main" configRootPath="/workspaces" />
-       <Workspace name="${wsp.name}">
-               <FileSystem class="org.apache.jackrabbit.core.fs.local.LocalFileSystem">
-                       <param name="path" value="${wsp.home}" />
-               </FileSystem>
-               <PersistenceManager
-                       class="org.apache.jackrabbit.core.persistence.bundle.BundleFsPersistenceManager">
-                       <param name="blobFSBlockSize" value="1" />
-               </PersistenceManager>
-               <SearchIndex class="org.apache.jackrabbit.core.query.lucene.SearchIndex">
-                       <param name="path" value="${rep.home}/repository/index" />
-               </SearchIndex>
-       </Workspace>
-
-       <!-- Versioning -->
-       <Versioning rootPath="${rep.home}/version">
-               <FileSystem class="org.apache.jackrabbit.core.fs.local.LocalFileSystem">
-                       <param name="path" value="${rep.home}/version" />
-               </FileSystem>
-               <PersistenceManager
-                       class="org.apache.jackrabbit.core.persistence.bundle.BundleFsPersistenceManager">
-                       <param name="blobFSBlockSize" value="1" />
-               </PersistenceManager>
-       </Versioning>
-
-       <!-- Indexing -->
-       <SearchIndex class="org.apache.jackrabbit.core.query.lucene.SearchIndex">
-               <param name="path" value="${rep.home}/repository/index" />
-               <param name="tikaConfigPath" value="tika-config.xml"/>
-       </SearchIndex>
-
-       <!-- Security -->
-       <Security appName="Jackrabbit">
-               <SecurityManager
-                       class="org.apache.jackrabbit.core.security.simple.SimpleSecurityManager"
-                       workspaceName="security" />
-               <AccessManager
-                       class="org.apache.jackrabbit.core.security.simple.SimpleAccessManager" />
-               <LoginModule
-                       class="org.apache.jackrabbit.core.security.simple.SimpleLoginModule">
-                       <param name="anonymousId" value="anonymous" />
-                       <param name="adminId" value="admin" />
-               </LoginModule>
-       </Security>
-</Repository>
\ No newline at end of file
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 (file)
index 7396c87..0000000
+++ /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<String, ?> sharedState, Map<String, ?> 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 (file)
index 9a49a06..0000000
+++ /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<JackrabbitDataModelMigration> {
-       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 (file)
index 77ad527..0000000
+++ /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 (file)
index 0f9db87..0000000
+++ /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 (file)
index 4b240f0..0000000
+++ /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 (file)
index e08f4d6..0000000
+++ /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<String, String> params = new HashMap<String, String>();
-                       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 (file)
index 3fb0db9..0000000
+++ /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<HttpHost, AuthScheme> cache;
-
-       public NonSerialBasicAuthCache() {
-               cache = new ConcurrentHashMap<HttpHost, AuthScheme>();
-       }
-
-       @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 (file)
index a2eb983..0000000
+++ /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 (file)
index 1cae6e4..0000000
+++ /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<String, JcrFileSystem> fileSystems = new HashMap<>();
-
-       @Override
-       public String getScheme() {
-               return "davex";
-       }
-
-       @Override
-       public FileSystem newFileSystem(URI uri, Map<String, ?> 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<String, String> params = new HashMap<String, String>();
-               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<String, Object>());
-                               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<Path> 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 (file)
index e3a70d0..0000000
+++ /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<String, ?> 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<String, Object>());
-                       } 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 (file)
index f2541fb..0000000
+++ /dev/null
@@ -1,57 +0,0 @@
-<?xml version="1.0"?>
-<!DOCTYPE Repository PUBLIC "Jackrabbit 2.6" "http://jackrabbit.apache.org/dtd/repository-2.6.dtd">
-<Repository>
-       <!-- File system and datastore -->
-       <FileSystem
-               class="org.apache.jackrabbit.core.fs.mem.MemoryFileSystem" />
-
-       <!-- Workspace templates -->
-       <Workspaces rootPath="${rep.home}/workspaces"
-               defaultWorkspace="main" configRootPath="/workspaces" />
-       <Workspace name="${wsp.name}">
-               <FileSystem
-                       class="org.apache.jackrabbit.core.fs.mem.MemoryFileSystem" />
-               <PersistenceManager
-                       class="org.apache.jackrabbit.core.persistence.bundle.BundleFsPersistenceManager">
-               </PersistenceManager>
-               <SearchIndex
-                       class="org.apache.jackrabbit.core.query.lucene.SearchIndex">
-                       <param name="path" value="${wsp.home}/index" />
-                       <param name="directoryManagerClass"
-                               value="org.apache.jackrabbit.core.query.lucene.directory.RAMDirectoryManager" />
-                       <param name="extractorPoolSize" value="0" />
-                       <FileSystem
-                               class="org.apache.jackrabbit.core.fs.mem.MemoryFileSystem" />
-               </SearchIndex>
-       </Workspace>
-
-       <!-- Versioning -->
-       <Versioning rootPath="${rep.home}/version">
-               <FileSystem
-                       class="org.apache.jackrabbit.core.fs.mem.MemoryFileSystem" />
-               <PersistenceManager
-                       class="org.apache.jackrabbit.core.persistence.bundle.BundleFsPersistenceManager">
-               </PersistenceManager>
-       </Versioning>
-
-       <!-- Indexing -->
-       <SearchIndex
-               class="org.apache.jackrabbit.core.query.lucene.SearchIndex">
-               <param name="path" value="${rep.home}/index" />
-               <param name="directoryManagerClass"
-                       value="org.apache.jackrabbit.core.query.lucene.directory.RAMDirectoryManager" />
-               <param name="extractorPoolSize" value="0" />
-               <FileSystem
-                       class="org.apache.jackrabbit.core.fs.mem.MemoryFileSystem" />
-       </SearchIndex>
-
-       <!-- Security -->
-       <Security appName="Jackrabbit">
-               <LoginModule
-                       class="org.apache.jackrabbit.core.security.SimpleLoginModule" />
-               <!-- <SecurityManager class="org.argeo.security.jackrabbit.ArgeoSecurityManager" -->
-               <!-- workspaceName="security" /> -->
-               <!-- <AccessManager class="org.argeo.security.jackrabbit.ArgeoAccessManager" 
-                       /> -->
-       </Security>
-</Repository>
\ 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 (file)
index c9ec2c3..0000000
+++ /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 (file)
index 17497d6..0000000
+++ /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 (file)
index 0526762..0000000
+++ /dev/null
@@ -1,82 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!DOCTYPE Repository PUBLIC "Jackrabbit 2.6" "http://jackrabbit.apache.org/dtd/repository-2.6.dtd">
-<Repository>
-       <!-- Shared datasource -->
-       <DataSources>
-               <DataSource name="dataSource">
-                       <param name="driver" value="org.h2.Driver" />
-                       <param name="url" value="${dburl}" />
-                       <param name="user" value="${dbuser}" />
-                       <param name="password" value="${dbpassword}" />
-                       <param name="databaseType" value="h2" />
-                       <param name="maxPoolSize" value="${maxPoolSize}" />
-               </DataSource>
-       </DataSources>
-
-       <!-- File system and datastore -->
-       <FileSystem class="org.apache.jackrabbit.core.fs.db.DbFileSystem">
-               <param name="dataSourceName" value="dataSource" />
-               <param name="schema" value="default" />
-               <param name="schemaObjectPrefix" value="fs_" />
-       </FileSystem>
-       <DataStore class="org.apache.jackrabbit.core.data.FileDataStore">
-               <param name="path" value="${rep.home}/datastore" />
-       </DataStore>
-
-       <!-- Workspace templates -->
-       <Workspaces rootPath="${rep.home}/workspaces"
-               defaultWorkspace="${defaultWorkspace}" />
-       <Workspace name="${wsp.name}">
-               <FileSystem class="org.apache.jackrabbit.core.fs.db.DbFileSystem">
-                       <param name="dataSourceName" value="dataSource" />
-                       <param name="schema" value="default" />
-                       <param name="schemaObjectPrefix" value="${wsp.name}_fs_" />
-               </FileSystem>
-               <PersistenceManager
-                       class="org.apache.jackrabbit.core.persistence.pool.H2PersistenceManager">
-                       <param name="dataSourceName" value="dataSource" />
-                       <param name="schemaObjectPrefix" value="${wsp.name}_pm_" />
-                       <param name="bundleCacheSize" value="${bundleCacheMB}" />
-               </PersistenceManager>
-               <SearchIndex class="org.apache.jackrabbit.core.query.lucene.SearchIndex">
-                       <param name="path" value="${wsp.home}/index" />
-                       <param name="extractorPoolSize" value="${extractorPoolSize}" />
-                       <param name="cacheSize" value="${searchCacheSize}" />
-                       <param name="maxVolatileIndexSize" value="${maxVolatileIndexSize}" />
-               </SearchIndex>
-               <WorkspaceSecurity>
-                       <AccessControlProvider
-                               class="org.argeo.security.jackrabbit.ArgeoAccessControlProvider" />
-               </WorkspaceSecurity>
-       </Workspace>
-
-       <!-- Versioning -->
-       <Versioning rootPath="${rep.home}/version">
-               <FileSystem class="org.apache.jackrabbit.core.fs.db.DbFileSystem">
-                       <param name="dataSourceName" value="dataSource" />
-                       <param name="schema" value="default" />
-                       <param name="schemaObjectPrefix" value="fs_ver_" />
-               </FileSystem>
-               <PersistenceManager
-                       class="org.apache.jackrabbit.core.persistence.pool.H2PersistenceManager">
-                       <param name="dataSourceName" value="dataSource" />
-                       <param name="schemaObjectPrefix" value="pm_ver_" />
-                       <param name="bundleCacheSize" value="${bundleCacheMB}" />
-               </PersistenceManager>
-       </Versioning>
-
-       <!-- Indexing -->
-       <SearchIndex class="org.apache.jackrabbit.core.query.lucene.SearchIndex">
-               <param name="path" value="${rep.home}/index" />
-               <param name="extractorPoolSize" value="${extractorPoolSize}" />
-               <param name="cacheSize" value="${searchCacheSize}" />
-               <param name="maxVolatileIndexSize" value="${maxVolatileIndexSize}" />
-       </SearchIndex>
-
-       <!-- Security -->
-       <Security appName="Jackrabbit">
-               <SecurityManager class="org.argeo.security.jackrabbit.ArgeoSecurityManager"
-                       workspaceName="security" />
-               <AccessManager class="org.argeo.security.jackrabbit.ArgeoAccessManager" />
-       </Security>
-</Repository>
\ 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 (file)
index 3d24708..0000000
+++ /dev/null
@@ -1,60 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!DOCTYPE Repository PUBLIC "Jackrabbit 2.6" "http://jackrabbit.apache.org/dtd/repository-2.6.dtd">
-<Repository>
-       <!-- File system and datastore -->
-       <FileSystem class="org.apache.jackrabbit.core.fs.local.LocalFileSystem">
-               <param name="path" value="${rep.home}/repository" />
-       </FileSystem>
-       <DataStore class="org.apache.jackrabbit.core.data.FileDataStore">
-               <param name="path" value="${rep.home}/datastore" />
-       </DataStore>
-
-       <!-- Workspace templates -->
-       <Workspaces rootPath="${rep.home}/workspaces"
-               defaultWorkspace="${defaultWorkspace}" />
-       <Workspace name="${wsp.name}">
-               <FileSystem class="org.apache.jackrabbit.core.fs.local.LocalFileSystem">
-                       <param name="path" value="${wsp.home}" />
-               </FileSystem>
-               <PersistenceManager
-                       class="org.apache.jackrabbit.core.persistence.bundle.BundleFsPersistenceManager">
-                       <param name="bundleCacheSize" value="${bundleCacheMB}" />
-               </PersistenceManager>
-               <SearchIndex class="org.apache.jackrabbit.core.query.lucene.SearchIndex">
-                       <param name="path" value="${wsp.home}/index" />
-                       <param name="extractorPoolSize" value="${extractorPoolSize}" />
-                       <param name="cacheSize" value="${searchCacheSize}" />
-                       <param name="maxVolatileIndexSize" value="${maxVolatileIndexSize}" />
-               </SearchIndex>
-               <WorkspaceSecurity>
-                       <AccessControlProvider
-                               class="org.argeo.security.jackrabbit.ArgeoAccessControlProvider" />
-               </WorkspaceSecurity>
-       </Workspace>
-
-       <!-- Versioning -->
-       <Versioning rootPath="${rep.home}/version">
-               <FileSystem class="org.apache.jackrabbit.core.fs.local.LocalFileSystem">
-                       <param name="path" value="${rep.home}/version" />
-               </FileSystem>
-               <PersistenceManager
-                       class="org.apache.jackrabbit.core.persistence.bundle.BundleFsPersistenceManager">
-                       <param name="bundleCacheSize" value="${bundleCacheMB}" />
-               </PersistenceManager>
-       </Versioning>
-
-       <!-- Indexing -->
-       <SearchIndex class="org.apache.jackrabbit.core.query.lucene.SearchIndex">
-               <param name="path" value="${rep.home}/index" />
-               <param name="extractorPoolSize" value="${extractorPoolSize}" />
-               <param name="cacheSize" value="${searchCacheSize}" />
-               <param name="maxVolatileIndexSize" value="${maxVolatileIndexSize}" />
-       </SearchIndex>
-
-       <!-- Security -->
-       <Security appName="Jackrabbit">
-               <SecurityManager class="org.argeo.security.jackrabbit.ArgeoSecurityManager"
-                       workspaceName="security" />
-               <AccessManager class="org.argeo.security.jackrabbit.ArgeoAccessManager" />
-       </Security>
-</Repository>
\ 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 (file)
index ecee5bd..0000000
+++ /dev/null
@@ -1,55 +0,0 @@
-<?xml version="1.0"?>
-<!DOCTYPE Repository PUBLIC "Jackrabbit 2.6" "http://jackrabbit.apache.org/dtd/repository-2.6.dtd">
-<Repository>
-       <!-- File system and datastore -->
-       <FileSystem class="org.apache.jackrabbit.core.fs.mem.MemoryFileSystem" />
-
-       <!-- Workspace templates -->
-       <Workspaces rootPath="${rep.home}/workspaces"
-               defaultWorkspace="${defaultWorkspace}" configRootPath="/workspaces" />
-       <Workspace name="${wsp.name}">
-               <FileSystem class="org.apache.jackrabbit.core.fs.mem.MemoryFileSystem" />
-               <PersistenceManager
-                       class="org.apache.jackrabbit.core.persistence.bundle.BundleFsPersistenceManager">
-                       <param name="blobFSBlockSize" value="1" />
-                       <param name="bundleCacheSize" value="${bundleCacheMB}" />
-               </PersistenceManager>
-               <SearchIndex class="org.apache.jackrabbit.core.query.lucene.SearchIndex">
-                       <param name="path" value="${wsp.home}/index" />
-                       <param name="directoryManagerClass"
-                               value="org.apache.jackrabbit.core.query.lucene.directory.RAMDirectoryManager" />
-                       <param name="extractorPoolSize" value="${extractorPoolSize}" />
-                       <param name="cacheSize" value="${searchCacheSize}" />
-                       <param name="maxVolatileIndexSize" value="${maxVolatileIndexSize}" />
-                       <FileSystem class="org.apache.jackrabbit.core.fs.mem.MemoryFileSystem" />
-               </SearchIndex>
-       </Workspace>
-
-       <!-- Versioning -->
-       <Versioning rootPath="${rep.home}/version">
-               <FileSystem class="org.apache.jackrabbit.core.fs.mem.MemoryFileSystem" />
-               <PersistenceManager
-                       class="org.apache.jackrabbit.core.persistence.bundle.BundleFsPersistenceManager">
-                       <param name="blobFSBlockSize" value="1" />
-                       <param name="bundleCacheSize" value="${bundleCacheMB}" />
-               </PersistenceManager>
-       </Versioning>
-
-       <!-- Indexing -->
-       <SearchIndex class="org.apache.jackrabbit.core.query.lucene.SearchIndex">
-               <param name="path" value="${rep.home}/index" />
-               <param name="directoryManagerClass"
-                       value="org.apache.jackrabbit.core.query.lucene.directory.RAMDirectoryManager" />
-               <param name="extractorPoolSize" value="${extractorPoolSize}" />
-               <param name="cacheSize" value="${searchCacheSize}" />
-               <param name="maxVolatileIndexSize" value="${maxVolatileIndexSize}" />
-               <FileSystem class="org.apache.jackrabbit.core.fs.mem.MemoryFileSystem" />
-       </SearchIndex>
-
-       <!-- Security -->
-       <Security appName="Jackrabbit">
-               <SecurityManager class="org.argeo.security.jackrabbit.ArgeoSecurityManager"
-                       workspaceName="security" />
-               <AccessManager class="org.argeo.security.jackrabbit.ArgeoAccessManager" />
-       </Security>
-</Repository>
\ 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 (file)
index 07a0d04..0000000
+++ /dev/null
@@ -1,82 +0,0 @@
-<?xml version="1.0"?>
-<!DOCTYPE Repository PUBLIC "Jackrabbit 2.6" "http://jackrabbit.apache.org/dtd/repository-2.6.dtd">
-<Repository>
-       <!-- Shared datasource -->
-       <DataSources>
-               <DataSource name="dataSource">
-                       <param name="driver" value="org.postgresql.Driver" />
-                       <param name="url" value="${dburl}" />
-                       <param name="user" value="${dbuser}" />
-                       <param name="password" value="${dbpassword}" />
-                       <param name="databaseType" value="postgresql" />
-                       <param name="maxPoolSize" value="${maxPoolSize}" />
-               </DataSource>
-       </DataSources>
-
-       <!-- File system and datastore -->
-       <FileSystem class="org.apache.jackrabbit.core.fs.db.DbFileSystem">
-               <param name="dataSourceName" value="dataSource" />
-               <param name="schema" value="postgresql" />
-               <param name="schemaObjectPrefix" value="fs_" />
-       </FileSystem>
-       <DataStore class="org.apache.jackrabbit.core.data.FileDataStore">
-               <param name="path" value="${rep.home}/datastore" />
-       </DataStore>
-
-       <!-- Workspace templates -->
-       <Workspaces rootPath="${rep.home}/workspaces"
-               defaultWorkspace="${defaultWorkspace}" />
-       <Workspace name="${wsp.name}">
-               <FileSystem class="org.apache.jackrabbit.core.fs.db.DbFileSystem">
-                       <param name="dataSourceName" value="dataSource" />
-                       <param name="schema" value="postgresql" />
-                       <param name="schemaObjectPrefix" value="${wsp.name}_fs_" />
-               </FileSystem>
-               <PersistenceManager
-                       class="org.apache.jackrabbit.core.persistence.pool.PostgreSQLPersistenceManager">
-                       <param name="dataSourceName" value="dataSource" />
-                       <param name="schemaObjectPrefix" value="${wsp.name}_pm_" />
-                       <param name="bundleCacheSize" value="${bundleCacheMB}" />
-               </PersistenceManager>
-               <SearchIndex class="org.apache.jackrabbit.core.query.lucene.SearchIndex">
-                       <param name="path" value="${wsp.home}/index" />
-                       <param name="extractorPoolSize" value="${extractorPoolSize}" />
-                       <param name="cacheSize" value="${searchCacheSize}" />
-                       <param name="maxVolatileIndexSize" value="${maxVolatileIndexSize}" />
-               </SearchIndex>
-               <WorkspaceSecurity>
-                       <AccessControlProvider
-                               class="org.argeo.security.jackrabbit.ArgeoAccessControlProvider" />
-               </WorkspaceSecurity>
-       </Workspace>
-
-       <!-- Versioning -->
-       <Versioning rootPath="${rep.home}/version">
-               <FileSystem class="org.apache.jackrabbit.core.fs.db.DbFileSystem">
-                       <param name="dataSourceName" value="dataSource" />
-                       <param name="schema" value="postgresql" />
-                       <param name="schemaObjectPrefix" value="fs_ver_" />
-               </FileSystem>
-               <PersistenceManager
-                       class="org.apache.jackrabbit.core.persistence.pool.PostgreSQLPersistenceManager">
-                       <param name="dataSourceName" value="dataSource" />
-                       <param name="schemaObjectPrefix" value="pm_ver_" />
-                       <param name="bundleCacheSize" value="${bundleCacheMB}" />
-               </PersistenceManager>
-       </Versioning>
-
-       <!-- Indexing -->
-       <SearchIndex class="org.apache.jackrabbit.core.query.lucene.SearchIndex">
-               <param name="path" value="${rep.home}/index" />
-               <param name="extractorPoolSize" value="${extractorPoolSize}" />
-               <param name="cacheSize" value="${searchCacheSize}" />
-               <param name="maxVolatileIndexSize" value="${maxVolatileIndexSize}" />
-       </SearchIndex>
-
-       <!-- Security -->
-       <Security appName="Jackrabbit">
-               <SecurityManager class="org.argeo.security.jackrabbit.ArgeoSecurityManager"
-                       workspaceName="security" />
-               <AccessManager class="org.argeo.security.jackrabbit.ArgeoAccessManager" />
-       </Security>
-</Repository>
\ 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 (file)
index 9677828..0000000
+++ /dev/null
@@ -1,79 +0,0 @@
-<?xml version="1.0"?>
-<!DOCTYPE Repository PUBLIC "Jackrabbit 2.6" "http://jackrabbit.apache.org/dtd/repository-2.6.dtd">
-<Repository>
-       <!-- Shared datasource -->
-       <DataSources>
-               <DataSource name="dataSource">
-                       <param name="driver" value="org.postgresql.Driver" />
-                       <param name="url" value="${dburl}" />
-                       <param name="user" value="${dbuser}" />
-                       <param name="password" value="${dbpassword}" />
-                       <param name="databaseType" value="postgresql" />
-                       <param name="maxPoolSize" value="${maxPoolSize}" />
-               </DataSource>
-       </DataSources>
-
-       <!-- File system and datastore -->
-       <FileSystem class="org.apache.jackrabbit.core.fs.db.DbFileSystem">
-               <param name="dataSourceName" value="dataSource" />
-               <param name="schema" value="postgresql" />
-               <param name="schemaObjectPrefix" value="fs_" />
-       </FileSystem>
-
-       <!-- Workspace templates -->
-       <Workspaces rootPath="${rep.home}/workspaces"
-               defaultWorkspace="${defaultWorkspace}" />
-       <Workspace name="${wsp.name}">
-               <FileSystem class="org.apache.jackrabbit.core.fs.db.DbFileSystem">
-                       <param name="dataSourceName" value="dataSource" />
-                       <param name="schema" value="postgresql" />
-                       <param name="schemaObjectPrefix" value="${wsp.name}_fs_" />
-               </FileSystem>
-               <PersistenceManager
-                       class="org.apache.jackrabbit.core.persistence.pool.PostgreSQLPersistenceManager">
-                       <param name="dataSourceName" value="dataSource" />
-                       <param name="schemaObjectPrefix" value="${wsp.name}_pm_" />
-                       <param name="bundleCacheSize" value="${bundleCacheMB}" />
-               </PersistenceManager>
-               <SearchIndex class="org.apache.jackrabbit.core.query.lucene.SearchIndex">
-                       <param name="path" value="${wsp.home}/index" />
-                       <param name="extractorPoolSize" value="${extractorPoolSize}" />
-                       <param name="cacheSize" value="${searchCacheSize}" />
-                       <param name="maxVolatileIndexSize" value="${maxVolatileIndexSize}" />
-               </SearchIndex>
-               <WorkspaceSecurity>
-                       <AccessControlProvider
-                               class="org.argeo.security.jackrabbit.ArgeoAccessControlProvider" />
-               </WorkspaceSecurity>
-       </Workspace>
-
-       <!-- Versioning -->
-       <Versioning rootPath="${rep.home}/version">
-               <FileSystem class="org.apache.jackrabbit.core.fs.db.DbFileSystem">
-                       <param name="dataSourceName" value="dataSource" />
-                       <param name="schema" value="postgresql" />
-                       <param name="schemaObjectPrefix" value="fs_ver_" />
-               </FileSystem>
-               <PersistenceManager
-                       class="org.apache.jackrabbit.core.persistence.pool.PostgreSQLPersistenceManager">
-                       <param name="dataSourceName" value="dataSource" />
-                       <param name="schemaObjectPrefix" value="pm_ver_" />
-                       <param name="bundleCacheSize" value="${bundleCacheMB}" />
-               </PersistenceManager>
-       </Versioning>
-
-       <!-- Indexing -->
-       <SearchIndex class="org.apache.jackrabbit.core.query.lucene.SearchIndex">
-               <param name="path" value="${rep.home}/index" />
-               <param name="extractorPoolSize" value="${extractorPoolSize}" />
-               <param name="cacheSize" value="${searchCacheSize}" />
-               <param name="maxVolatileIndexSize" value="${maxVolatileIndexSize}" />
-       </SearchIndex>
-
-       <!-- Security -->
-       <Security appName="Jackrabbit">
-               <SecurityManager class="org.argeo.security.jackrabbit.ArgeoSecurityManager"
-                       workspaceName="security" />
-               <AccessManager class="org.argeo.security.jackrabbit.ArgeoAccessManager" />
-       </Security>
-</Repository>
\ 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 (file)
index a75c795..0000000
+++ /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<Privilege> privileges = new ArrayList<Privilege>();
-               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<Privilege> 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 (file)
index f3a282c..0000000
+++ /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 (file)
index f65432e..0000000
+++ /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 (file)
index 0313f91..0000000
+++ /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 (file)
index 3b6143b..0000000
+++ /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 (file)
index 348dc28..0000000
+++ /dev/null
@@ -1,81 +0,0 @@
-<?xml version="1.0"?>
-<!DOCTYPE Repository PUBLIC "-//The Apache Software Foundation//DTD Jackrabbit 1.6//EN"
-                            "http://jackrabbit.apache.org/dtd/repository-2.0.dtd">
-<Repository>
-       <!-- Shared datasource -->
-       <DataSources>
-               <DataSource name="dataSource">
-                       <param name="driver" value="org.h2.Driver" />
-                       <param name="url" value="jdbc:h2:mem:jackrabbit" />
-                       <param name="user" value="sa" />
-                       <param name="password" value="" />
-                       <param name="databaseType" value="h2" />
-                       <param name="maxPoolSize" value="10" />
-               </DataSource>
-       </DataSources>
-
-       <!-- File system and datastore -->
-       <FileSystem class="org.apache.jackrabbit.core.fs.db.DbFileSystem">
-               <param name="dataSourceName" value="dataSource" />
-               <param name="schema" value="default" />
-               <param name="schemaObjectPrefix" value="fs_" />
-       </FileSystem>
-       <DataStore class="org.apache.jackrabbit.core.data.db.DbDataStore">
-               <param name="dataSourceName" value="dataSource" />
-               <param name="schemaObjectPrefix" value="ds_" />
-       </DataStore>
-
-       <!-- Workspace templates -->
-       <Workspaces rootPath="${rep.home}/workspaces"
-               defaultWorkspace="dev" />
-       <Workspace name="${wsp.name}">
-               <FileSystem class="org.apache.jackrabbit.core.fs.db.DbFileSystem">
-                       <param name="dataSourceName" value="dataSource" />
-                       <param name="schema" value="default" />
-                       <param name="schemaObjectPrefix" value="${wsp.name}_fs_" />
-               </FileSystem>
-               <PersistenceManager
-                       class="org.apache.jackrabbit.core.persistence.pool.H2PersistenceManager">
-                       <param name="dataSourceName" value="dataSource" />
-                       <param name="schemaObjectPrefix" value="${wsp.name}_pm_" />
-               </PersistenceManager>
-               <SearchIndex class="org.apache.jackrabbit.core.query.lucene.SearchIndex">
-                       <param name="path" value="${wsp.home}/index" />
-               </SearchIndex>
-       </Workspace>
-
-       <!-- Versioning -->
-       <Versioning rootPath="${rep.home}/version">
-               <FileSystem class="org.apache.jackrabbit.core.fs.db.DbFileSystem">
-                       <param name="dataSourceName" value="dataSource" />
-                       <param name="schema" value="default" />
-                       <param name="schemaObjectPrefix" value="fs_ver_" />
-               </FileSystem>
-               <PersistenceManager
-                       class="org.apache.jackrabbit.core.persistence.pool.H2PersistenceManager">
-                       <param name="dataSourceName" value="dataSource" />
-                       <param name="schemaObjectPrefix" value="pm_ver_" />
-               </PersistenceManager>
-       </Versioning>
-
-       <!-- Indexing -->
-       <SearchIndex class="org.apache.jackrabbit.core.query.lucene.SearchIndex">
-               <param name="path" value="${rep.home}/repository/index" />
-               <param name="extractorPoolSize" value="2" />
-               <param name="supportHighlighting" value="true" />
-       </SearchIndex>
-
-       <!-- Security -->
-       <Security appName="Jackrabbit">
-               <SecurityManager
-                       class="org.apache.jackrabbit.core.security.simple.SimpleSecurityManager"
-                       workspaceName="security" />
-               <AccessManager
-                       class="org.apache.jackrabbit.core.security.simple.SimpleAccessManager" />
-               <LoginModule
-                       class="org.apache.jackrabbit.core.security.simple.SimpleLoginModule">
-                       <param name="anonymousId" value="anonymous" />
-                       <param name="adminId" value="admin" />
-               </LoginModule>
-       </Security>
-</Repository>
\ No newline at end of file
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 (file)
index 8395424..0000000
+++ /dev/null
@@ -1,72 +0,0 @@
-<?xml version="1.0"?>
-<!--
-
-    Copyright (C) 2007-2012 Argeo GmbH
-
-    Licensed under the Apache License, Version 2.0 (the "License");
-    you may not use this file except in compliance with the License.
-    You may obtain a copy of the License at
-
-            http://www.apache.org/licenses/LICENSE-2.0
-
-    Unless required by applicable law or agreed to in writing, software
-    distributed under the License is distributed on an "AS IS" BASIS,
-    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-    See the License for the specific language governing permissions and
-    limitations under the License.
-
--->
-<!DOCTYPE Repository PUBLIC "-//The Apache Software Foundation//DTD Jackrabbit 1.6//EN"
-                            "http://jackrabbit.apache.org/dtd/repository-2.0.dtd">
-<Repository>
-       <!-- File system and datastore -->
-       <FileSystem class="org.apache.jackrabbit.core.fs.mem.MemoryFileSystem" />
-
-       <!-- Workspace templates -->
-       <Workspaces rootPath="${rep.home}/workspaces"
-               defaultWorkspace="main" configRootPath="/workspaces" />
-       <Workspace name="${wsp.name}">
-               <FileSystem class="org.apache.jackrabbit.core.fs.mem.MemoryFileSystem" />
-               <PersistenceManager
-                       class="org.apache.jackrabbit.core.persistence.bundle.BundleFsPersistenceManager">
-                       <param name="blobFSBlockSize" value="1" />
-               </PersistenceManager>
-               <SearchIndex class="org.apache.jackrabbit.core.query.lucene.SearchIndex">
-                       <param name="path" value="${rep.home}/repository/index" />
-                       <param name="directoryManagerClass"
-                               value="org.apache.jackrabbit.core.query.lucene.directory.RAMDirectoryManager" />
-                       <FileSystem class="org.apache.jackrabbit.core.fs.mem.MemoryFileSystem" />
-               </SearchIndex>
-       </Workspace>
-
-       <!-- Versioning -->
-       <Versioning rootPath="${rep.home}/version">
-               <FileSystem class="org.apache.jackrabbit.core.fs.mem.MemoryFileSystem" />
-               <PersistenceManager
-                       class="org.apache.jackrabbit.core.persistence.bundle.BundleFsPersistenceManager">
-                       <param name="blobFSBlockSize" value="1" />
-               </PersistenceManager>
-       </Versioning>
-
-       <!-- Indexing -->
-       <SearchIndex class="org.apache.jackrabbit.core.query.lucene.SearchIndex">
-               <param name="path" value="${rep.home}/repository/index" />
-               <param name="directoryManagerClass"
-                       value="org.apache.jackrabbit.core.query.lucene.directory.RAMDirectoryManager" />
-               <FileSystem class="org.apache.jackrabbit.core.fs.mem.MemoryFileSystem" />
-       </SearchIndex>
-
-       <!-- Security -->
-       <Security appName="Jackrabbit">
-               <SecurityManager
-                       class="org.apache.jackrabbit.core.security.simple.SimpleSecurityManager"
-                       workspaceName="security" />
-               <AccessManager
-                       class="org.apache.jackrabbit.core.security.simple.SimpleAccessManager" />
-               <LoginModule
-                       class="org.apache.jackrabbit.core.security.simple.SimpleLoginModule">
-                       <param name="anonymousId" value="anonymous" />
-                       <param name="adminId" value="admin" />
-               </LoginModule>
-       </Security>
-</Repository>
\ No newline at end of file
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 (file)
index 0984276..0000000
+++ /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 (file)
index 84eea1f..0000000
+++ /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, <code>null</code> 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 (file)
index d77bd49..0000000
+++ /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 (file)
index a578c45..0000000
+++ /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 (file)
index dc2963a..0000000
+++ /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<Session>() {
-
-                               @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 (file)
index c6e7415..0000000
+++ /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 (file)
index 0418810..0000000
+++ /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 (file)
index b4124ee..0000000
+++ /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<Node> iterator;
-       private Integer position = 0;
-
-       public CollectionNodeIterator(Collection<Node> 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 (file)
index fc68888..0000000
+++ /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 (file)
index 72e325d..0000000
+++ /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 <code>null</code>
- * 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 <code>Files</code> 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";
-       /**
-        * <code>jcr:name</code>, 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";
-       /**
-        * <code>jcr:path</code>, 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";
-       /**
-        * <code>jcr:primaryType</code> with prefix instead of namespace (as in
-        * {@link Property#JCR_PRIMARY_TYPE}.
-        */
-       public final static String JCR_PRIMARY_TYPE = "jcr:primaryType";
-       /**
-        * <code>jcr:mixinTypes</code> with prefix instead of namespace (as in
-        * {@link Property#JCR_MIXIN_TYPES}.
-        */
-       public final static String JCR_MIXIN_TYPES = "jcr:mixinTypes";
-       /**
-        * <code>jcr:uuid</code> with prefix instead of namespace (as in
-        * {@link Property#JCR_UUID}.
-        */
-       public final static String JCR_UUID = "jcr:uuid";
-       /**
-        * <code>jcr:created</code> with prefix instead of namespace (as in
-        * {@link Property#JCR_CREATED}.
-        */
-       public final static String JCR_CREATED = "jcr:created";
-       /**
-        * <code>jcr:createdBy</code> with prefix instead of namespace (as in
-        * {@link Property#JCR_CREATED_BY}.
-        */
-       public final static String JCR_CREATED_BY = "jcr:createdBy";
-       /**
-        * <code>jcr:lastModified</code> with prefix instead of namespace (as in
-        * {@link Property#JCR_LAST_MODIFIED}.
-        */
-       public final static String JCR_LAST_MODIFIED = "jcr:lastModified";
-       /**
-        * <code>jcr:lastModifiedBy</code> 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<Node> iterate(NodeIterator nodeIterator) {
-               return new Iterable<Node>() {
-
-                       @Override
-                       public Iterator<Node> 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<Node> 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<Node> getNodes(Node node) {
-               List<Node> 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 <code>null</node> 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 <code>null</node> 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 <code>null</code>, the primary
-        *                    type will be used (typically for XML structures)
-        * @param primaryType the primary type, if <code>null</code>
-        *                    {@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 <code>null</code>
-        * @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 <code>null</node> 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
-        * <code>null</code>.
-        * 
-        * @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
-        *         <code>null</code> 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
-        *         <code>defaultValue</code> 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 <code>null</code> 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
-        *         <code>defaultValue</code> 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> 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 <T> List<T> 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 <T> List<T> getMultiple(Property p) {
-               try {
-                       List<T> 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<Version> getLinearVersions(VersionHistory versionHistory) {
-               try {
-                       List<Version> 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 <code>null</code> 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 <code>null</code> 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 (file)
index 351929f..0000000
+++ /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<br/>
-        * value := group1,group2,user1
-        */
-       private Map<String, String> principalPrivileges = new HashMap<String, String>();
-
-       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<Privilege> privs = new ArrayList<Privilege>();
-                       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<Privilege> 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<String, String> groupPrivileges) {
-               this.principalPrivileges = groupPrivileges;
-       }
-
-       public void setPrincipalPrivileges(Map<String, String> 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 (file)
index efbaabe..0000000
+++ /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<Session, Object> {
-       /** @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 (file)
index c778743..0000000
+++ /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 (file)
index 71cf961..0000000
+++ /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 <code>UNKNOWN</code> 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 <code>true</code> if cancellation has been requested, and
-        *         <code>false</code> otherwise
-        * @see #setCanceled(boolean)
-        */
-       public boolean isCanceled();
-
-       /**
-        * Sets the cancel state to the given value.
-        * 
-        * @param value
-        *            <code>true</code> indicates that cancelation has been
-        *            requested (but not necessarily acknowledged);
-        *            <code>false</code> 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 (file)
index 3228eee..0000000
+++ /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<String, String> 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<String> 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 (file)
index 82a65e7..0000000
+++ /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 (file)
index 3be8be1..0000000
+++ /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<String> 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<Node> nodeIteratorToList(NodeIterator nodeIterator) {
-               List<Node> nodes = new ArrayList<Node>();
-               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<String> 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<String> 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<String> tokenize(String path) {
-               List<String> tokens = new ArrayList<String>();
-               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<String, PropertyDiff> diffProperties(Node reference, Node observed) {
-               Map<String, PropertyDiff> diffs = new TreeMap<String, PropertyDiff>();
-               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<String, PropertyDiff> 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<String, PropertyDiff> diffProperties(Node reference, Node observed, List<String> properties) {
-               Map<String, PropertyDiff> diffs = new TreeMap<String, PropertyDiff>();
-               try {
-                       Iterator<String> 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 <code>false</code>, 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,
-        * <a href="https://issues.apache.org/jira/browse/JCR-2233">these properties are
-        * not automatically updated</a>, 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
-        * <a href="http://jackrabbit.apache.org/node-type-notation.html"> Node type
-        * Notation </a> 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<Privilege> privileges = new ArrayList<Privilege>();
-               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<Privilege> 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("</");
-                       sb.append(nodeName);
-                       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 (file)
index 666b259..0000000
+++ /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<Value> 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<String> 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<String> getChecksums(Node node) {
-               try {
-                       List<String> 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 (file)
index 9dd43ad..0000000
+++ /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 (file)
index 0cbad33..0000000
+++ /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 (file)
index 71e76fe..0000000
+++ /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 (file)
index 4f42f2d..0000000
+++ /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 (file)
index 1e23338..0000000
+++ /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<Repository> repositories;
-
-       private ThreadLocal<Session> session = new ThreadLocal<Session>();
-       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<Thread> threads = Collections.synchronizedList(new ArrayList<Thread>());
-       private final Map<Long, Session> activeSessions = Collections.synchronizedMap(new HashMap<Long, Session>());
-       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<Thread> 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<? extends Session> 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<Repository> 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<Repository> 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 (file)
index dab5554..0000000
+++ /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<String, PropertyDiff> diffs;
-       private Calendar updateTime;
-
-       public VersionDiff(String userId, Calendar updateTime,
-                       Map<String, PropertyDiff> diffs) {
-               this.userId = userId;
-               this.updateTime = updateTime;
-               this.diffs = diffs;
-       }
-
-       public String getUserId() {
-               return userId;
-       }
-
-       public Map<String, PropertyDiff> 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 (file)
index d6550fe..0000000
+++ /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 (file)
index 7c9711b..0000000
+++ /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 (file)
index 3d538e8..0000000
+++ /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<String, WorkspaceFileStore> 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<JcrPath> listDirectMounts(Path base) {
-               String baseStr = base.toString();
-               Set<JcrPath> 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<Path> getRootDirectories() {
-               Set<Path> single = new HashSet<>();
-               single.add(new JcrPath(this, JcrPath.separator));
-               return single;
-       }
-
-       @Override
-       public Iterable<FileStore> getFileStores() {
-               List<FileStore> stores = new ArrayList<>();
-               stores.add(baseFileStore);
-               stores.addAll(mounts.values());
-               return stores;
-       }
-
-       @Override
-       public Set<String> supportedFileAttributeViews() {
-               try {
-                       String[] prefixes = session.getNamespacePrefixes();
-                       Set<String> 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 (file)
index 74d9a19..0000000
+++ /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<? extends OpenOption> 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<Path> newDirectoryStream(Path dir, Filter<? super Path> 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 extends FileAttributeView> V getFileAttributeView(Path path, Class<V> type, LinkOption... options) {
-               throw new UnsupportedOperationException();
-       }
-
-       @SuppressWarnings("unchecked")
-       @Override
-       public <A extends BasicFileAttributes> A readAttributes(Path path, Class<A> 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<String, Object> readAttributes(Path path, String attributes, LinkOption... options) throws IOException {
-               try {
-                       Node node = toNode(path);
-                       String pattern = attributes.replace(',', '|');
-                       Map<String, Object> res = new HashMap<String, Object>();
-                       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 (file)
index f214fdc..0000000
+++ /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 (file)
index 1a4d747..0000000
+++ /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<Path> iterator() {
-               return new Iterator<Path>() {
-                       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 (file)
index eda07a5..0000000
+++ /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<Path> {
-       private final JcrFileSystem fs;
-       private final NodeIterator nodeIterator;
-       private final Iterator<JcrPath> additionalPaths;
-       private final Filter<? super Path> filter;
-
-       public NodeDirectoryStream(JcrFileSystem fs, NodeIterator nodeIterator, Iterator<JcrPath> additionalPaths,
-                       Filter<? super Path> filter) {
-               this.fs = fs;
-               this.nodeIterator = nodeIterator;
-               this.additionalPaths = additionalPaths;
-               this.filter = filter;
-       }
-
-       @Override
-       public void close() throws IOException {
-       }
-
-       @Override
-       public Iterator<Path> iterator() {
-               return new Iterator<Path>() {
-                       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 (file)
index 8054d52..0000000
+++ /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 (file)
index 4643c8c..0000000
+++ /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;
-
-/**
- * <b>Hacked from org.apache.jackrabbit.util.Text in Jackrabbit JCR Commons</b>
- * 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 <code>true</code>, 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<String> strings = new ArrayList<String>();
-               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 <code>oldString</code> in <code>text</code>
-        * with <code>newString</code>.
-        *
-        * @param text
-        * @param oldString
-        *            old substring to be replaced with <code>newString</code>
-        * @param newString
-        *            new substring to replace occurrences of <code>oldString</code>
-        * @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("&lt;");
-                       } else if (ch == '>') {
-                               buf.append("&gt;");
-                       } else if (ch == '&') {
-                               buf.append("&amp;");
-                       } else if (ch == '"') {
-                               buf.append("&quot;");
-                       } else if (ch == '\'') {
-                               buf.append(isHtml ? "&#39;" : "&apos;");
-                       }
-               }
-               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 <code>escape()</code>
-        * and <code>unescape()</code> METHODS. They contains the characters as
-        * defined 'unreserved' in section 2.3 of the RFC 2396 'URI generic syntax':
-        * <p>
-        * 
-        * <pre>
-        * unreserved  = alphanum | mark
-        * mark        = "-" | "_" | "." | "!" | "~" | "*" | "'" | "(" | ")"
-        * </pre>
-        */
-       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 <code>string</code> using the
-        * <code>escape</code> 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 <code>string</code> is <code>null</code>.
-        */
-       public static String escape(String string, char escape) {
-               return escape(string, escape, false);
-       }
-
-       /**
-        * Does an URL encoding of the <code>string</code> using the
-        * <code>escape</code> 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 <code>isPath</code> is
-        * <code>true</code>, additionally the slash '/' is ignored, too.
-        *
-        * @param string
-        *            the string to encode.
-        * @param escape
-        *            the escape character.
-        * @param isPath
-        *            if <code>true</code>, the string is treated as path
-        * @return the escaped string
-        * @throws NullPointerException
-        *             if <code>string</code> is <code>null</code>.
-        */
-       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 <code>string</code>. 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 <code>string</code> is <code>null</code>.
-        */
-       public static String escape(String string) {
-               return escape(string, '%');
-       }
-
-       /**
-        * Does a URL encoding of the <code>path</code>. 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 <code>path</code> is <code>null</code>.
-        */
-       public static String escapePath(String path) {
-               return escape(path, '%', true);
-       }
-
-       /**
-        * Does a URL decoding of the <code>string</code> using the
-        * <code>escape</code> 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 <code>string</code> is <code>null</code>.
-        * @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 <code>string</code>. 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 <code>string</code> is <code>null</code>.
-        * @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.
-        * <p>
-        * QName EBNF:<br>
-        * <xmp> 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 *) </xmp>
-        *
-        * @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.
-        * <p>
-        * QName EBNF:<br>
-        * <xmp> 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 *) </xmp>
-        *
-        * @since Apache Jackrabbit 2.3.2 and 2.2.10
-        * @see <a href=
-        *      "https://issues.apache.org/jira/browse/JCR-3128">JCR-3128</a>
-        * @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.
-        * <p>
-        * Example:<br>
-        * 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.
-        * <p>
-        * 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 <code>null</code> if <code>path</code> is
-        *         <code>null</code>.
-        */
-       public static String getName(String path) {
-               return getName(path, '/');
-       }
-
-       /**
-        * Returns the name part of the path, delimited by the given
-        * <code>delim</code>. If the given path is already a name (i.e. contains no
-        * <code>delim</code> characters) it is returned.
-        *
-        * @param path
-        *            the path
-        * @param delim
-        *            the delimiter
-        * @return the name part or <code>null</code> if <code>path</code> is
-        *         <code>null</code>.
-        */
-       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 <code>qname</code>. If the
-        * prefix is missing, an empty string is returned. Please note, that this
-        * method does not validate the name or prefix.
-        * </p>
-        * 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 <code>qname</code> is <code>null</code>
-        */
-       public static String getNamespacePrefix(String qname) {
-               int pos = qname.indexOf(':');
-               return pos >= 0 ? qname.substring(0, pos) : "";
-       }
-
-       /**
-        * Returns the local name of the given <code>qname</code>. Please note, that
-        * this method does not validate the name.
-        * </p>
-        * the qname has the format: qname := [prefix ':'] local;
-        *
-        * @param qname
-        *            a qualified name
-        * @return the localname
-        *
-        * @see #getNamespacePrefix(String)
-        *
-        * @throws NullPointerException
-        *             if <code>qname</code> is <code>null</code>
-        */
-       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 <code>descendant</code> path is hierarchical a
-        * descendant of <code>path</code>.
-        *
-        * @param path
-        *            the current path
-        * @param descendant
-        *            the potential descendant
-        * @return <code>true</code> if the <code>descendant</code> is a descendant;
-        *         <code>false</code> 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 <code>descendant</code> path is hierarchical a
-        * descendant of <code>path</code> or equal to it.
-        *
-        * @param path
-        *            the path to check
-        * @param descendant
-        *            the potential descendant
-        * @return <code>true</code> if the <code>descendant</code> is a descendant
-        *         or equal; <code>false</code> 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 n<sup>th</sup> relative parent of the path, where n=level.
-        * <p>
-        * Example:<br>
-        * <code>
-        * Text.getRelativeParent("/foo/bar/test", 1) == "/foo/bar"
-        * </code>
-        *
-        * @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 n<sup>th</sup> absolute parent of the path, where n=level.
-        * <p>
-        * Example:<br>
-        * <code>
-        * Text.getAbsoluteParent("/foo/bar/test", 1) == "/foo/bar"
-        * </code>
-        *
-        * @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
-        * <code>${...}</code> 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
-        * <code>ignoreMissing</code> is <code>true</code>. In the later case, the
-        * missing variable is replaced by the empty string.
-        *
-        * @param value
-        *            the original value
-        * @param ignoreMissing
-        *            if <code>true</code>, 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 (file)
index 6d9d05c..0000000
+++ /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<? extends FileAttributeView> type) {
-               return false;
-       }
-
-       @Override
-       public boolean supportsFileAttributeView(String name) {
-               return false;
-       }
-
-       @Override
-       public <V extends FileStoreAttributeView> V getFileStoreAttributeView(Class<V> 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 (file)
index 0cdfdaf..0000000
+++ /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 (file)
index 3eb0e7a..0000000
+++ /dev/null
@@ -1,16 +0,0 @@
-//
-// JCR EXTENSIONS
-//
-<jcrx = "http://www.argeo.org/ns/jcrx">
-
-[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 (file)
index 1837749..0000000
+++ /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 (file)
index 2adb6a9..0000000
+++ /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
-        * <code>false</code>.
-        */
-       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 <code>null</code> 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
-        *                             <code>false</code>, 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<String, String> 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<String, String> 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("</");
-               writer.append(tagName);
-               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 (file)
index 813d065..0000000
+++ /dev/null
@@ -1,19 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" ?>
-<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
-  <xsl:output method="xml" indent="yes"/>
-  <xsl:template match="/|comment()|processing-instruction()">
-    <xsl:copy>
-      <xsl:apply-templates/>
-    </xsl:copy>
-  </xsl:template>
-  <xsl:template match="*">
-    <xsl:element name="{local-name()}">
-      <xsl:apply-templates select="@*|node()"/>
-    </xsl:element>
-  </xsl:template>
-  <xsl:template match="@*">
-    <xsl:attribute name="{local-name()}">
-      <xsl:value-of select="."/>
-    </xsl:attribute>
-  </xsl:template>
-</xsl:stylesheet>
\ 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 (file)
index 6003d63..0000000
+++ /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<String> 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<String> 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<String> getRequiredRoles() {
-               return new HashSet<>();
-       }
-
-       public void destroy() {
-
-       }
-
-       /*
-        * UTILITIES
-        */
-
-       /** Create these roles as group if they don't exist. */
-       protected void makeSureRolesExists(EnumSet<? extends Distinguished> enumSet) {
-               makeSureRolesExists(Distinguished.enumToDns(enumSet));
-       }
-
-       /** Create these roles as group if they don't exist. */
-       protected void makeSureRolesExists(Set<String> 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 (file)
index a30fe97..0000000
+++ /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<String> roles = new ArrayList<String>();
-       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<String> 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 (file)
index ef83c1f..0000000
+++ /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<String> 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("<!-- ");
-                                               out.write(getCurrentJcrPath());
-                                               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("</");
-                               out.write(SV_PREFIX + ":" + localName);
-                               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<String> 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 (file)
index 60e8f8e..0000000
+++ /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<String, String> 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<Set<String>> 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<Set<String>> contentPathsFuture = executorService
-                                               .submit(() -> performNodeBackup(defaultWorkspace, nodePath));
-                               executorService.submit(() -> performFilesBackup(defaultWorkspace, contentPathsFuture));
-                       }
-               } finally {
-                       Jcr.logout(session);
-               }
-
-       }
-
-       protected Set<String> 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<String> 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<Set<String>> contentPathsFuture) {
-               Set<String> 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 <remote URL> [<target directory>]");
-
-       }
-
-       protected static Repository createRemoteRepository(URI uri) throws RepositoryException {
-               RepositoryFactory repositoryFactory = new ClientDavexRepositoryFactory();
-               Map<String, String> params = new HashMap<String, String>();
-               params.put(ClientDavexRepositoryFactory.JACKRABBIT_DAVEX_URI, uri.toString());
-               // 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<String, String> headers = bundle.getHeaders();
-                       Manifest manifest = new Manifest();
-                       Enumeration<String> 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<URL> 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 (file)
index a12bb41..0000000
+++ /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<Path> 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<Path> workspaceDirs = Files.newDirectoryStream(workspaces)) {
-                               for (Path workspacePath : workspaceDirs) {
-                                       String workspaceName = workspacePath.getFileName().toString();
-                                       Session session = JcrUtils.loginOrCreateWorkspace(repository, workspaceName);
-                                       try (DirectoryStream<Path> 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 (file)
index a61e19b..0000000
+++ /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 (file)
index ef40ab3..0000000
+++ /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 (file)
index 1ce974c..0000000
+++ /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 d6f35ab9c1c1339892e20910f3ffa68ad2c63a09..c4d70f610cf27acff274a65a56c4839e9418cd19 100644 (file)
--- a/pom.xml
+++ b/pom.xml
@@ -19,7 +19,7 @@
        <modules>
                <!-- Base -->
                <module>org.argeo.enterprise</module>
-               <module>org.argeo.jcr</module>
+<!--           <module>org.argeo.jcr</module> -->
                <module>org.argeo.osgi.boot</module>
                <module>org.argeo.core</module>
                <!-- Eclipse -->
@@ -27,8 +27,9 @@
                <module>org.argeo.eclipse.ui.rap</module>
                <!-- CMS -->
                <module>org.argeo.api</module>
-               <module>org.argeo.maintenance</module>
+<!--           <module>org.argeo.maintenance</module> -->
                <module>org.argeo.cms</module>
+               <module>org.argeo.cms.jcr</module>
                <module>org.argeo.cms.ui.theme</module>
                <module>org.argeo.cms.ui</module>
                <module>org.argeo.cms.ui.rap</module>