From: Mathieu Baudier Date: Sat, 29 Feb 2020 08:38:12 +0000 (+0100) Subject: Introduce multi-workspaces JCR file system. X-Git-Tag: argeo-commons-2.1.87~2 X-Git-Url: https://git.argeo.org/?p=lgpl%2Fargeo-commons.git;a=commitdiff_plain;h=6d8432bcf931c7525ecc4254b2a95e8a413a06f1 Introduce multi-workspaces JCR file system. --- diff --git a/org.argeo.jcr/ext/test/org/argeo/jcr/fs/JcrFileSystemTest.java b/org.argeo.jcr/ext/test/org/argeo/jcr/fs/JcrFileSystemTest.java index 190eb949a..886efa367 100644 --- a/org.argeo.jcr/ext/test/org/argeo/jcr/fs/JcrFileSystemTest.java +++ b/org.argeo.jcr/ext/test/org/argeo/jcr/fs/JcrFileSystemTest.java @@ -13,11 +13,14 @@ 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; @@ -25,6 +28,38 @@ 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 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); + } + public void testSimple() throws Exception { FileSystemProvider fsProvider = new JackrabbitMemoryFsProvider(); diff --git a/org.argeo.jcr/src/org/argeo/jackrabbit/fs/JackrabbitMemoryFsProvider.java b/org.argeo.jcr/src/org/argeo/jackrabbit/fs/JackrabbitMemoryFsProvider.java index 47cf33d14..e3a70d084 100644 --- a/org.argeo.jcr/src/org/argeo/jackrabbit/fs/JackrabbitMemoryFsProvider.java +++ b/org.argeo.jcr/src/org/argeo/jackrabbit/fs/JackrabbitMemoryFsProvider.java @@ -10,6 +10,8 @@ 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; @@ -23,6 +25,13 @@ 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"; @@ -32,12 +41,11 @@ public class JackrabbitMemoryFsProvider extends AbstractJackrabbitFsProvider { public FileSystem newFileSystem(URI uri, Map env) throws IOException { try { Path tempDir = Files.createTempDirectory("fs-memory"); - URL confUrl = getClass().getResource("fs-memory.xml"); + URL confUrl = JackrabbitMemoryFsProvider.class.getResource("fs-memory.xml"); RepositoryConfig repositoryConfig = RepositoryConfig.create(confUrl.toURI(), tempDir.toString()); repository = RepositoryImpl.create(repositoryConfig); - String username = System.getProperty("user.name"); - Session session = repository.login(new SimpleCredentials(username, username.toCharArray())); - fileSystem = new JcrFileSystem(this, session); + postRepositoryCreation(repository); + fileSystem = new JcrFileSystem(this, repository, credentials); return fileSystem; } catch (RepositoryException | URISyntaxException e) { throw new IOException("Cannot login to repository", e); @@ -61,4 +69,19 @@ public class JackrabbitMemoryFsProvider extends AbstractJackrabbitFsProvider { 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.jcr/src/org/argeo/jcr/fs/JcrFileSystem.java b/org.argeo.jcr/src/org/argeo/jcr/fs/JcrFileSystem.java index 67371d08f..4c8355f97 100644 --- a/org.argeo.jcr/src/org/argeo/jcr/fs/JcrFileSystem.java +++ b/org.argeo.jcr/src/org/argeo/jcr/fs/JcrFileSystem.java @@ -8,10 +8,16 @@ 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.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; @@ -20,12 +26,19 @@ import org.argeo.jcr.JcrUtils; public class JcrFileSystem extends FileSystem { private final JcrFileSystemProvider provider; + private final Session session; + private WorkspaceFileStore baseFileStore; + + private Map mounts = new TreeMap<>(); + private String userHomePath = null; + @Deprecated public JcrFileSystem(JcrFileSystemProvider provider, Session session) throws IOException { super(); this.provider = provider; + baseFileStore = new WorkspaceFileStore(null, session.getWorkspace()); this.session = session; Node userHome = provider.getUserHome(session); if (userHome != null) @@ -36,6 +49,38 @@ public class JcrFileSystem extends FileSystem { } } + 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; + 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 + 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(session); + if (userHome != null) + try { + userHomePath = userHome.getPath(); + } catch (RepositoryException e) { + throw new IOException("Cannot retrieve user home path", e); + } + } + /** Whether this node should be skipped in directory listings */ public boolean skipNode(Node node) throws RepositoryException { if (node.isNodeType(NodeType.NT_HIERARCHY_NODE)) @@ -47,6 +92,34 @@ public class JcrFileSystem extends FileSystem { return userHomePath; } + public WorkspaceFileStore getFileStore(String path) { + WorkspaceFileStore res = baseFileStore; + for (String mountPath : mounts.keySet()) { + if (path.startsWith(mountPath)) { + 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 WorkspaceFileStore getBaseFileStore() { + return baseFileStore; + } + @Override public FileSystemProvider provider() { return provider; @@ -55,6 +128,14 @@ public class JcrFileSystem extends FileSystem { @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 @@ -69,23 +150,22 @@ public class JcrFileSystem extends FileSystem { @Override public String getSeparator() { - return "/"; + return JcrPath.separator; } @Override public Iterable getRootDirectories() { - try { - Set single = new HashSet<>(); - single.add(new JcrPath(this, session.getRootNode())); - return single; - } catch (RepositoryException e) { - throw new JcrFsException("Cannot get root path", e); - } + Set single = new HashSet<>(); + single.add(new JcrPath(this, JcrPath.separator)); + return single; } @Override public Iterable getFileStores() { - throw new UnsupportedOperationException(); + List stores = new ArrayList<>(); + stores.add(baseFileStore); + stores.addAll(mounts.values()); + return stores; } @Override diff --git a/org.argeo.jcr/src/org/argeo/jcr/fs/JcrFileSystemProvider.java b/org.argeo.jcr/src/org/argeo/jcr/fs/JcrFileSystemProvider.java index bd0befe7c..a9ea14826 100644 --- a/org.argeo.jcr/src/org/argeo/jcr/fs/JcrFileSystemProvider.java +++ b/org.argeo.jcr/src/org/argeo/jcr/fs/JcrFileSystemProvider.java @@ -188,8 +188,8 @@ public abstract class JcrFileSystemProvider extends FileSystemProvider { @Override public FileStore getFileStore(Path path) throws IOException { - Session session = ((JcrFileSystem) path.getFileSystem()).getSession(); - return new WorkSpaceFileStore(session.getWorkspace()); + JcrFileSystem fileSystem = (JcrFileSystem) path.getFileSystem(); + return fileSystem.getFileStore(path.toString()); } @Override @@ -320,4 +320,5 @@ public abstract class JcrFileSystemProvider extends FileSystemProvider { public Node getUserHome(Session session) { return null; } + } diff --git a/org.argeo.jcr/src/org/argeo/jcr/fs/JcrPath.java b/org.argeo.jcr/src/org/argeo/jcr/fs/JcrPath.java index c5d86f679..1a4d74706 100644 --- a/org.argeo.jcr/src/org/argeo/jcr/fs/JcrPath.java +++ b/org.argeo.jcr/src/org/argeo/jcr/fs/JcrPath.java @@ -17,14 +17,15 @@ import java.util.NoSuchElementException; import javax.jcr.Node; import javax.jcr.RepositoryException; -import javax.jcr.Session; /** A {@link Path} which contains a reference to a JCR {@link Node}. */ public class JcrPath implements Path { - private final static String delimStr = "/"; - private final static char delimChar = '/'; + 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; @@ -35,14 +36,16 @@ public class JcrPath implements Path { this.fs = filesSystem; if (path == null) throw new JcrFsException("Path cannot be null"); - if (path.equals(delimStr)) {// root + 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; } @@ -53,26 +56,36 @@ public class JcrPath implements Path { throw new JcrFsException("No home directory available"); } - this.absolute = path.charAt(0) == delimChar ? true : false; + 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) == delimChar ? path.length() - 1 : path.length()); - this.path = trimmedPath.split(delimStr); + 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, node.getPath()); + this(filesSystem, filesSystem.getFileStore(node).toFsPath(node)); } /** Internal optimisation */ - private JcrPath(JcrFileSystem filesSystem, String[] path, boolean absolute) { + 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 @@ -87,31 +100,17 @@ public class JcrPath implements Path { @Override public Path getRoot() { - try { - if (path == null) - return this; - return new JcrPath(fs, fs.getSession().getRootNode()); - } catch (RepositoryException e) { - throw new JcrFsException("Cannot get root", e); - } + if (path == null) + return this; + return new JcrPath(fs, separator); } @Override public String toString() { - 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(); + return toFsPath(path); } - public String toJcrPath() { + private String toFsPath(String[] path) { if (path == null) return "/"; StringBuilder sb = new StringBuilder(); @@ -120,11 +119,31 @@ public class JcrPath implements Path { for (int i = 0; i < path.length; i++) { if (i != 0) sb.append('/'); - sb.append(Text.escapeIllegalJcrChars(path[i])); + 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) @@ -137,9 +156,12 @@ public class JcrPath implements Path { if (path == null) return null; if (path.length == 1)// root - return new JcrPath(fs, delimStr); + return new JcrPath(fs, separator); String[] parentPath = Arrays.copyOfRange(path, 0, path.length - 1); - return new JcrPath(fs, parentPath, absolute); + if (!absolute) + return new JcrPath(fs, null, parentPath, absolute); + else + return new JcrPath(fs, toFsPath(parentPath)); } @Override @@ -161,7 +183,7 @@ public class JcrPath implements Path { if (path == null) return null; String[] parentPath = Arrays.copyOfRange(path, beginIndex, endIndex); - return new JcrPath(fs, parentPath, false); + return new JcrPath(fs, null, parentPath, false); } @Override @@ -204,7 +226,11 @@ public class JcrPath implements Path { System.arraycopy(path, 0, newPath, 0, path.length); System.arraycopy(otherPath.path, 0, newPath, path.length, otherPath.path.length); } - return new JcrPath(fs, newPath, absolute); + if (!absolute) + return new JcrPath(fs, null, newPath, absolute); + else { + return new JcrPath(fs, toFsPath(newPath)); + } } @Override @@ -281,7 +307,7 @@ public class JcrPath implements Path { public Path toAbsolutePath() { if (isAbsolute()) return this; - return new JcrPath(fs, path, true); + return new JcrPath(fs, fileStore, path, true); } @Override @@ -313,13 +339,15 @@ public class JcrPath implements Path { public Node getNode() throws RepositoryException { if (!isAbsolute())// TODO default dir - throw new JcrFsException("Cannot get node from relative path"); - String pathStr = toJcrPath(); - Session session = fs.getSession(); - // TODO synchronize on the session ? - if (!session.itemExists(pathStr)) - return null; - return session.getNode(pathStr); + 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 diff --git a/org.argeo.jcr/src/org/argeo/jcr/fs/WorkSpaceFileStore.java b/org.argeo.jcr/src/org/argeo/jcr/fs/WorkSpaceFileStore.java deleted file mode 100644 index bdefff3e4..000000000 --- a/org.argeo.jcr/src/org/argeo/jcr/fs/WorkSpaceFileStore.java +++ /dev/null @@ -1,67 +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 javax.jcr.Workspace; - -public class WorkSpaceFileStore extends FileStore { - private Workspace workspace; - - public WorkSpaceFileStore(Workspace workspace) { - this.workspace = workspace; - } - - @Override - public String name() { - return workspace.getName(); - } - - @Override - public String type() { - return "workspace"; - } - - @Override - public boolean isReadOnly() { - return false; - } - - @Override - public long getTotalSpace() throws IOException { - return 0; - } - - @Override - public long getUsableSpace() throws IOException { - return 0; - } - - @Override - public long getUnallocatedSpace() throws IOException { - return 0; - } - - @Override - public boolean supportsFileAttributeView(Class type) { - return false; - } - - @Override - public boolean supportsFileAttributeView(String name) { - return false; - } - - @Override - public V getFileStoreAttributeView(Class type) { - return null; - } - - @Override - public Object getAttribute(String attribute) throws IOException { - return workspace.getSession().getRepository().getDescriptor(attribute); - } - -} diff --git a/org.argeo.jcr/src/org/argeo/jcr/fs/WorkspaceFileStore.java b/org.argeo.jcr/src/org/argeo/jcr/fs/WorkspaceFileStore.java new file mode 100644 index 000000000..0b81d5569 --- /dev/null +++ b/org.argeo.jcr/src/org/argeo/jcr/fs/WorkspaceFileStore.java @@ -0,0 +1,149 @@ +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 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; + } + + public void close() { + JcrUtils.logoutQuietly(workspace.getSession()); + } + + @Override + public String name() { + return workspace.getName(); + } + + @Override + public String type() { + return "workspace"; + } + + @Override + public boolean isReadOnly() { + return false; + } + + @Override + public long getTotalSpace() throws IOException { + return 0; + } + + @Override + public long getUsableSpace() throws IOException { + return 0; + } + + @Override + public long getUnallocatedSpace() throws IOException { + return 0; + } + + @Override + public boolean supportsFileAttributeView(Class type) { + return false; + } + + @Override + public boolean supportsFileAttributeView(String name) { + return false; + } + + @Override + public V getFileStoreAttributeView(Class type) { + return null; + } + + @Override + public Object getAttribute(String attribute) throws IOException { + return workspace.getSession().getRepository().getDescriptor(attribute); + } + + public Workspace getWorkspace() { + return workspace; + } + + public String toFsPath(Node node) throws RepositoryException { + String nodeWorkspaceName = node.getSession().getWorkspace().getName(); + if (!nodeWorkspaceName.equals(workspace.getName())) + throw new IllegalArgumentException("Icompatible " + node + " from workspace '" + nodeWorkspaceName + + "' in file store '" + workspace.getName() + "'"); + return mountPath == null ? node.getPath() : mountPath + node.getPath(); + } + + public boolean isBase() { + return mountPath == null; + } + + Node toNode(String[] fullPath) throws RepositoryException { + String jcrPath = toJcrPath(fullPath); + Session session = workspace.getSession(); + if (!session.itemExists(jcrPath)) + return null; + Node node = session.getNode(jcrPath); + return node; + } + + 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(); + } + +}