From: Mathieu Baudier Date: Sat, 5 Nov 2016 20:11:04 +0000 (+0000) Subject: First working JCR file system X-Git-Tag: argeo-commons-2.1.51~17 X-Git-Url: http://git.argeo.org/?a=commitdiff_plain;h=d8037dd6a59ff5d38d7c7182a9ef6c26c8001a4f;p=lgpl%2Fargeo-commons.git First working JCR file system git-svn-id: https://svn.argeo.org/commons/trunk@9310 4cfe0d0a-d680-48aa-b62c-e0a02a3f76cc --- 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 new file mode 100644 index 000000000..1cf7ef70e --- /dev/null +++ b/org.argeo.jcr/ext/test/org/argeo/jcr/fs/JcrFileSystemTest.java @@ -0,0 +1,97 @@ +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.nodetype.NodeType; + +import org.apache.commons.io.IOUtils; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +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 testSimple() throws Exception { + FileSystemProvider fsProvider = new JackrabbitMemoryFsProvider(); + + // Simple file + Path testPath = fsProvider.getPath(new URI("jcr+memory:/test.txt")); + 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 rootPath = fsProvider.getPath(new URI("jcr+memory:/")); + log.debug("Got root " + rootPath); + 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); + try (OutputStream out = Files.newOutputStream(copiedFile); InputStream in = Files.newInputStream(testPath)) { + IOUtils.copy(in, out); + } + log.debug("Copied " + testPath + " to " + copiedFile); + Files.delete(testPath); + log.debug("Deleted " + testPath); + byte[] copiedRead = Files.readAllBytes(copiedFile); + assertTrue(Arrays.equals(copiedRead, read)); + log.debug("Read " + copiedFile); + // Browse directories + DirectoryStream files = Files.newDirectoryStream(testDir); + int fileCount = 0; + Path listedFile = null; + for (Path file : files) { + fileCount++; + if (!Files.isDirectory(file)) + listedFile = file; + } + assertEquals(2, fileCount); + assertEquals(copiedFile, listedFile); + assertEquals(copiedFile.toString(), listedFile.toString()); + log.debug("Listed " + testDir); + // Generic attributes + Map attrs = Files.readAttributes(copiedFile, "*"); + assertEquals(5, 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(6, attrs.size()); + log.debug("Read attributes of " + copiedFile + ": " + attrs.keySet()); + } +} diff --git a/org.argeo.jcr/ext/test/org/argeo/server/jcr/JcrResourceAdapterTest.java b/org.argeo.jcr/ext/test/org/argeo/server/jcr/JcrResourceAdapterTest.java index 3ce499af8..11dc4fab0 100644 --- a/org.argeo.jcr/ext/test/org/argeo/server/jcr/JcrResourceAdapterTest.java +++ b/org.argeo.jcr/ext/test/org/argeo/server/jcr/JcrResourceAdapterTest.java @@ -28,6 +28,7 @@ import org.argeo.jcr.JcrResourceAdapter; import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.Resource; +@Deprecated public class JcrResourceAdapterTest extends AbstractJackrabbitTestCase { private static SimpleDateFormat sdf = new SimpleDateFormat( "yyyyMMdd:hhmmss.SSS"); diff --git a/org.argeo.jcr/repository.xml b/org.argeo.jcr/repository.xml new file mode 100644 index 000000000..745079e2a --- /dev/null +++ b/org.argeo.jcr/repository.xml @@ -0,0 +1,152 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/org.argeo.jcr/repository/repository/meta/rootUUID b/org.argeo.jcr/repository/repository/meta/rootUUID new file mode 100644 index 000000000..df092932a --- /dev/null +++ b/org.argeo.jcr/repository/repository/meta/rootUUID @@ -0,0 +1 @@ +cafebabe-cafe-babe-cafe-babecafebabe \ No newline at end of file diff --git a/org.argeo.jcr/repository/repository/namespaces/ns_idx.properties b/org.argeo.jcr/repository/repository/namespaces/ns_idx.properties new file mode 100644 index 000000000..7e757f0aa --- /dev/null +++ b/org.argeo.jcr/repository/repository/namespaces/ns_idx.properties @@ -0,0 +1,8 @@ +#Fri Oct 28 20:14:30 CEST 2016 +http\://www.jcp.org/jcr/1.0=1570322 +internal=16762557 +http\://www.jcp.org/jcr/sv/1.0=16463688 +http\://www.jcp.org/jcr/mix/1.0=14361695 +http\://www.jcp.org/jcr/nt/1.0=5688619 +.empty.key=0 +http\://www.w3.org/XML/1998/namespace=6829023 diff --git a/org.argeo.jcr/repository/repository/namespaces/ns_reg.properties b/org.argeo.jcr/repository/repository/namespaces/ns_reg.properties new file mode 100644 index 000000000..f40bf219b --- /dev/null +++ b/org.argeo.jcr/repository/repository/namespaces/ns_reg.properties @@ -0,0 +1,8 @@ +#Fri Oct 28 20:14:30 CEST 2016 +jcr=http\://www.jcp.org/jcr/1.0 +sv=http\://www.jcp.org/jcr/sv/1.0 +xml=http\://www.w3.org/XML/1998/namespace +nt=http\://www.jcp.org/jcr/nt/1.0 +mix=http\://www.jcp.org/jcr/mix/1.0 +rep=internal +.empty.key= diff --git a/org.argeo.jcr/repository/workspaces/default/workspace.xml b/org.argeo.jcr/repository/workspaces/default/workspace.xml new file mode 100644 index 000000000..a32f9c756 --- /dev/null +++ b/org.argeo.jcr/repository/workspaces/default/workspace.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + diff --git a/org.argeo.jcr/src/org/argeo/jackrabbit/fs/AbstractJackrabbitFsProvider.java b/org.argeo.jcr/src/org/argeo/jackrabbit/fs/AbstractJackrabbitFsProvider.java new file mode 100644 index 000000000..a2eb98302 --- /dev/null +++ b/org.argeo.jcr/src/org/argeo/jackrabbit/fs/AbstractJackrabbitFsProvider.java @@ -0,0 +1,7 @@ +package org.argeo.jackrabbit.fs; + +import org.argeo.jcr.fs.JcrFileSystemProvider; + +public abstract class AbstractJackrabbitFsProvider extends JcrFileSystemProvider { + +} diff --git a/org.argeo.jcr/src/org/argeo/jackrabbit/fs/JackrabbitMemoryFsProvider.java b/org.argeo.jcr/src/org/argeo/jackrabbit/fs/JackrabbitMemoryFsProvider.java new file mode 100644 index 000000000..0ed826894 --- /dev/null +++ b/org.argeo.jcr/src/org/argeo/jackrabbit/fs/JackrabbitMemoryFsProvider.java @@ -0,0 +1,62 @@ +package org.argeo.jackrabbit.fs; + +import java.io.IOException; +import java.net.URI; +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.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; + + @Override + public String getScheme() { + return "jcr+memory"; + } + + @Override + public FileSystem newFileSystem(URI uri, Map env) throws IOException { + try { + Path tempDir = Files.createTempDirectory("fs-memory"); + URL confUrl = getClass().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); + return fileSystem; + } catch (Exception e) { + throw new JcrFsException("Cannot login to repository", e); + } + } + + @Override + public FileSystem getFileSystem(URI uri) { + return fileSystem; + } + + @Override + public Path getPath(URI uri) { + String path = uri.getPath(); + if(fileSystem==null) + try { + newFileSystem(uri, new HashMap()); + } catch (IOException e) { + throw new JcrFsException("Could not autocreate file system",e); + } + return fileSystem.getPath(path); + } + +} diff --git a/org.argeo.jcr/src/org/argeo/jackrabbit/fs/fs-memory.xml b/org.argeo.jcr/src/org/argeo/jackrabbit/fs/fs-memory.xml new file mode 100644 index 000000000..c7bab0d69 --- /dev/null +++ b/org.argeo.jcr/src/org/argeo/jackrabbit/fs/fs-memory.xml @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/org.argeo.jcr/src/org/argeo/jcr/JcrResourceAdapter.java b/org.argeo.jcr/src/org/argeo/jcr/JcrResourceAdapter.java index 1ccce4f76..be7bf4959 100644 --- a/org.argeo.jcr/src/org/argeo/jcr/JcrResourceAdapter.java +++ b/org.argeo.jcr/src/org/argeo/jcr/JcrResourceAdapter.java @@ -37,6 +37,7 @@ import org.apache.commons.logging.LogFactory; * Bridge Spring resources and JCR folder / files semantics (nt:folder / * nt:file), supporting versioning as well. */ +@Deprecated public class JcrResourceAdapter { private final static Log log = LogFactory.getLog(JcrResourceAdapter.class); diff --git a/org.argeo.jcr/src/org/argeo/jcr/fs/BinaryChannel.java b/org.argeo.jcr/src/org/argeo/jcr/fs/BinaryChannel.java new file mode 100644 index 000000000..ff4b4a1b3 --- /dev/null +++ b/org.argeo.jcr/src/org/argeo/jcr/fs/BinaryChannel.java @@ -0,0 +1,197 @@ +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.apache.commons.io.IOUtils; +import org.argeo.jcr.JcrUtils; + +public class BinaryChannel implements SeekableByteChannel { + private final Node file; + private Binary binary; + private boolean open = true; + + private long position = 0; + + // private ByteBuffer toWrite; + private FileChannel fc = null; + + public BinaryChannel(Node file) throws RepositoryException, IOException { + this.file = file; + // int capacity = 1024 * 1024; + // this.toWrite = ByteBuffer.allocate(capacity); + if (file.isNodeType(NodeType.NT_FILE)) { + if (file.hasNode(Property.JCR_CONTENT)) { + Node data = file.getNode(Property.JCR_CONTENT); + this.binary = data.getProperty(Property.JCR_DATA).getBinary(); + } else { + Node data = file.addNode(Property.JCR_CONTENT, NodeType.NT_RESOURCE); + try (InputStream in = new ByteArrayInputStream(new byte[0])) { + this.binary = data.getSession().getValueFactory().createBinary(in); + } + } + } 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(); + // byte[] arr = new byte[(int) position]; + // toWrite.flip(); + // toWrite.get(arr); + fc.position(0); + InputStream in = Channels.newInputStream(fc); + newBinary = session.getValueFactory().createBinary(in); + file.getNode(Property.JCR_CONTENT).setProperty(Property.JCR_DATA, newBinary); + session.save(); + open = false; + } catch (RepositoryException e) { + throw new JcrFsException("Cannot close " + file, e); + }finally{ + JcrUtils.closeQuietly(newBinary); + IOUtils.closeQuietly(fc); + } + } else { + clearReadState(); + open = false; + } + } + + @Override + public int read(ByteBuffer dst) throws IOException { + if (isModified()) { + return fc.read(dst); + } else { + + try { + int read; +// int capacity = dst.capacity(); + byte[] arr = dst.array(); + read = binary.read(arr, position); + //dst.put(arr, 0, read); + + // try { + // byte[] arr = dst.array(); + // read = binary.read(arr, position); + // } catch (UnsupportedOperationException e) { + // int capacity = dst.capacity(); + // byte[] arr = new byte[capacity]; + // read = binary.read(arr, position); + // dst.put(arr); + // } + if (read != -1) + position = position + read; + return read; + } catch (RepositoryException e) { + throw new JcrFsException("Cannot read into buffer", e); + } + } + } + + @Override + public int write(ByteBuffer src) throws IOException { + int written = getFileChannel().write(src); + return written; + // int byteCount = src.remaining(); + // if (toWrite.remaining() < byteCount) + // throw new JcrFsException("Write buffer is full"); + // toWrite.put(src); + // if (position < binarySize) + // position = binarySize + byteCount; + // else + // position = position + byteCount; + // return byteCount; + } + + @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 JcrFsException("Cannot get size", e); + } + } + } + + @Override + public SeekableByteChannel truncate(long size) throws IOException { + getFileChannel().truncate(size); + // if (size != size()) + // throw new UnsupportedOperationException("Cannot truncate JCR + // binary"); + 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 JcrFsException("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 new file mode 100644 index 000000000..e59abbc70 --- /dev/null +++ b/org.argeo.jcr/src/org/argeo/jcr/fs/JcrBasicfileAttributes.java @@ -0,0 +1,112 @@ +package org.argeo.jcr.fs; + +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 FileTime EPOCH = FileTime.fromMillis(0); + + public JcrBasicfileAttributes(Node node) { + this.node = node; + } + + @Override + public FileTime lastModifiedTime() { + try { + 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.isNodeType(NodeType.MIX_CREATED)) { + Instant instant = node.getProperty(Property.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 { + return node.isNodeType(NodeType.NT_FOLDER); + } 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_DATA).getProperty(Property.JCR_CONTENT).getBinary(); + return binary.getSize(); + } catch (RepositoryException e) { + throw new JcrFsException("Cannot check size", e); + } finally { + JcrUtils.closeQuietly(binary); + } + } + return -1; + } + + @Override + public Object fileKey() { + return null; + } + + @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 index 40328e8a0..885eaf19f 100644 --- a/org.argeo.jcr/src/org/argeo/jcr/fs/JcrFileSystem.java +++ b/org.argeo.jcr/src/org/argeo/jcr/fs/JcrFileSystem.java @@ -8,8 +8,10 @@ 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.HashSet; import java.util.Set; +import javax.jcr.RepositoryException; import javax.jcr.Session; import org.argeo.jcr.JcrUtils; @@ -51,43 +53,56 @@ public class JcrFileSystem extends FileSystem { @Override public Iterable getRootDirectories() { - return null; + 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); + } } @Override public Iterable getFileStores() { - // TODO Auto-generated method stub - return null; + throw new UnsupportedOperationException(); } @Override public Set supportedFileAttributeViews() { - // TODO Auto-generated method stub - return null; + try { + String[] prefixes = session.getNamespacePrefixes(); + Set res = new HashSet<>(); + for (String prefix : prefixes) + res.add(prefix); + res.add("basic"); + return res; + } catch (RepositoryException e) { + throw new JcrFsException("Cannot get supported file attributes views", e); + } } @Override public Path getPath(String first, String... more) { - // TODO Auto-generated method stub - return null; + 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) { - // TODO Auto-generated method stub - return null; + throw new UnsupportedOperationException(); } @Override public UserPrincipalLookupService getUserPrincipalLookupService() { - // TODO Auto-generated method stub - return null; + throw new UnsupportedOperationException(); } @Override public WatchService newWatchService() throws IOException { - // TODO Auto-generated method stub - return null; + throw new UnsupportedOperationException(); } public Session getSession() { 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 8ea4cca2e..3e07697dc 100644 --- a/org.argeo.jcr/src/org/argeo/jcr/fs/JcrFileSystemProvider.java +++ b/org.argeo.jcr/src/org/argeo/jcr/fs/JcrFileSystemProvider.java @@ -1,142 +1,261 @@ package org.argeo.jcr.fs; import java.io.IOException; -import java.net.URI; 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.FileSystem; 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; -public class JcrFileSystemProvider extends FileSystemProvider { +import javax.jcr.Node; +import javax.jcr.Property; +import javax.jcr.PropertyIterator; +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.nodetype.NodeType; +import javax.jcr.nodetype.PropertyDefinition; - @Override - public String getScheme() { - return "jcr"; - } +import org.apache.commons.io.FileExistsException; +import org.argeo.jcr.JcrUtils; +public abstract class JcrFileSystemProvider extends FileSystemProvider { @Override - public FileSystem newFileSystem(URI uri, Map env) + public SeekableByteChannel newByteChannel(Path path, Set options, FileAttribute... attrs) throws IOException { - // TODO Auto-generated method stub - return null; - } + try { + Node node = toNode(path); + if (node == null) { + Node parentNode = toNode(path.getParent()); + if (parentNode == null) + throw new JcrFsException("No parent directory for " + path); + if (!(parentNode.getPath().equals("/") || parentNode.isNodeType(NodeType.NT_FOLDER))) + throw new JcrFsException("Parent of " + path + " is not a directory"); - @Override - public FileSystem getFileSystem(URI uri) { - // TODO Auto-generated method stub - return null; + node = parentNode.addNode(path.getFileName().toString(), 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); + } catch (RepositoryException e) { + throw new JcrFsException("Cannot read file", e); + } } @Override - public Path getPath(URI uri) { - // TODO Auto-generated method stub - return null; + public DirectoryStream newDirectoryStream(Path dir, Filter filter) throws IOException { + try { + Node base = toNode(dir); + return new NodeDirectoryStream((JcrFileSystem) dir.getFileSystem(), base.getNodes(), filter); + } catch (RepositoryException e) { + throw new JcrFsException("Cannot list directory", e); + } } @Override - public SeekableByteChannel newByteChannel(Path path, - Set options, FileAttribute... attrs) - throws IOException { - // TODO Auto-generated method stub - return null; - } - - @Override - public DirectoryStream newDirectoryStream(Path dir, - Filter filter) throws IOException { - // TODO Auto-generated method stub - return null; - } - - @Override - public void createDirectory(Path dir, FileAttribute... attrs) - throws IOException { - // TODO Auto-generated method stub + public void createDirectory(Path dir, FileAttribute... attrs) throws IOException { + try { + Node node = toNode(dir); + if (node == null) { + Node parent = toNode(dir.getParent()); + if (parent == null) + throw new IOException("Parent of " + dir + " does not exist"); + if (!(parent.getPath().equals("/") || parent.getPrimaryNodeType().isNodeType(NodeType.NT_FOLDER))) + throw new JcrFsException(dir + " parent is not a directory"); + node = parent.addNode(dir.getFileName().toString(), NodeType.NT_FOLDER); + node.addMixin(NodeType.MIX_CREATED); + node.addMixin(NodeType.MIX_LAST_MODIFIED); + node.getSession().save(); + } else { + if (!node.getPrimaryNodeType().isNodeType(NodeType.NT_FOLDER)) + throw new FileExistsException(dir + " exists and is not a directory"); + } + } catch (RepositoryException e) { + throw new JcrFsException("Cannot create directory " + dir, e); + } } @Override public void delete(Path path) throws IOException { - // TODO Auto-generated method stub + try { + Node node = toNode(path); + if (node == null) + throw new NoSuchFileException(path + " does not exist"); + Session session = node.getSession(); + 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(); + } + session.save(); + } catch (RepositoryException e) { + throw new JcrFsException("Cannot delete " + path, e); + } } @Override - public void copy(Path source, Path target, CopyOption... options) - throws IOException { - // TODO Auto-generated method stub - + public void copy(Path source, Path target, CopyOption... options) throws IOException { + try { + Node sourceNode = toNode(source); + Node targetNode = toNode(target); + JcrUtils.copy(sourceNode, targetNode); + sourceNode.getSession().save(); + } catch (RepositoryException e) { + throw new JcrFsException("Cannot copy from " + source + " to " + target, e); + } } @Override - public void move(Path source, Path target, CopyOption... options) - throws IOException { - // TODO Auto-generated method stub - + public void move(Path source, Path target, CopyOption... options) throws IOException { + try { + Node sourceNode = toNode(source); + Session session = sourceNode.getSession(); + session.move(sourceNode.getPath(), target.toString()); + session.save(); + } catch (RepositoryException e) { + throw new JcrFsException("Cannot move from " + source + " to " + target, e); + } } @Override public boolean isSameFile(Path path, Path path2) throws IOException { - // TODO Auto-generated method stub - return false; + 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 JcrFsException("Cannot check whether " + path + " and " + path2 + " are same", e); + } + } + } @Override public boolean isHidden(Path path) throws IOException { - // TODO Auto-generated method stub return false; } @Override public FileStore getFileStore(Path path) throws IOException { - // TODO Auto-generated method stub - return null; + Session session = ((JcrFileSystem) path.getFileSystem()).getSession(); + return new WorkSpaceFileStore(session.getWorkspace()); } @Override public void checkAccess(Path path, AccessMode... modes) throws IOException { - // TODO Auto-generated method stub - + try { + Session session = ((JcrFileSystem) path.getFileSystem()).getSession(); + if (!session.itemExists(path.toString())) + throw new NoSuchFileException(path + " does not exist"); + // TODO check access via JCR api + } catch (RepositoryException e) { + throw new JcrFsException("Cannot delete " + path, e); + } } @Override - public V getFileAttributeView(Path path, - Class type, LinkOption... options) { - // TODO Auto-generated method stub - return null; + public V getFileAttributeView(Path path, Class type, LinkOption... options) { + throw new UnsupportedOperationException(); } + @SuppressWarnings("unchecked") @Override - public A readAttributes(Path path, - Class type, LinkOption... options) throws IOException { - // TODO Auto-generated method stub - return null; + public A readAttributes(Path path, Class type, LinkOption... options) + throws IOException { + try { + // TODO check if assignable + Node node = toNode(path); + return (A) new JcrBasicfileAttributes(node); + } catch (RepositoryException e) { + throw new JcrFsException("Cannot read basic attributes of " + path, e); + } } @Override - public Map readAttributes(Path path, String attributes, - LinkOption... options) throws IOException { - // TODO Auto-generated method stub - return null; + public Map readAttributes(Path path, String attributes, LinkOption... options) throws IOException { + try { + Node node = toNode(path); + String pattern = attributes.replace(',', '|'); + Map res = new HashMap(); + PropertyIterator it = node.getProperties(pattern); + props: while (it.hasNext()) { + Property prop = it.nextProperty(); + PropertyDefinition pd = prop.getDefinition(); + if (pd.isMultiple()) + continue props; + int requiredType = pd.getRequiredType(); + switch (requiredType) { + case PropertyType.LONG: + res.put(prop.getName(), prop.getLong()); + break; + case PropertyType.DOUBLE: + res.put(prop.getName(), prop.getDouble()); + break; + case PropertyType.BOOLEAN: + res.put(prop.getName(), prop.getBoolean()); + break; + case PropertyType.DATE: + res.put(prop.getName(), prop.getDate()); + break; + case PropertyType.BINARY: + byte[] arr = JcrUtils.getBinaryAsBytes(prop); + res.put(prop.getName(), arr); + break; + default: + res.put(prop.getName(), prop.getString()); + } + } + return res; + } catch (RepositoryException e) { + throw new JcrFsException("Cannot read attributes of " + path, e); + } } @Override - public void setAttribute(Path path, String attribute, Object value, - LinkOption... options) throws IOException { - // TODO Auto-generated method stub + public void setAttribute(Path path, String attribute, Object value, LinkOption... options) throws IOException { + try { + Node node = toNode(path); + 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()); + } + node.getSession().save(); + } catch (RepositoryException e) { + throw new JcrFsException("Cannot set attribute " + attribute + " on " + path, e); + } + } + protected Node toNode(Path path) throws RepositoryException { + return ((JcrPath) path).getNode(); } } 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 e25293517..6a0c7adfe 100644 --- a/org.argeo.jcr/src/org/argeo/jcr/fs/JcrPath.java +++ b/org.argeo.jcr/src/org/argeo/jcr/fs/JcrPath.java @@ -3,6 +3,7 @@ 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; @@ -10,147 +11,251 @@ 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; +import javax.jcr.Session; public class JcrPath implements Path { - private JcrFileSystem filesSystem; - private String path; + private final static String delimStr = "/"; + private final static char delimChar = '/'; + + private final JcrFileSystem fs; + private final String[] path;// null means root + private final boolean absolute; + + // optim + private final int hashCode; + + public JcrPath(JcrFileSystem filesSystem, String path) { + // this(filesSystem, path.equals("/") ? null : path.split("/"), path == + // null ? true : path.startsWith("/")); + this.fs = filesSystem; + if (path == null) + throw new JcrFsException("Path cannot be null"); + if (path.equals(delimStr)) {// root + this.path = null; + this.absolute = true; + this.hashCode = 0; + return; + } + this.absolute = path.charAt(0) == delimChar ? true : false; + String trimmedPath = path.substring(absolute ? 1 : 0, + path.charAt(path.length() - 1) == delimChar ? path.length() - 1 : path.length()); + this.path = trimmedPath.split(delimStr); + this.hashCode = this.path[this.path.length - 1].hashCode(); + } - private Node node; + public JcrPath(JcrFileSystem filesSystem, Node node) throws RepositoryException { + this(filesSystem, node.getPath()); + } - public JcrPath(JcrFileSystem filesSystem, Node node) { - super(); - this.filesSystem = filesSystem; - this.node = node; + /** Internal optimisation */ + private JcrPath(JcrFileSystem filesSystem, String[] path, boolean absolute) { + this.fs = filesSystem; + this.path = path; + this.absolute = path == null ? true : absolute; + this.hashCode = path == null ? 0 : path[path.length - 1].hashCode(); } @Override public FileSystem getFileSystem() { - return filesSystem; + return fs; } @Override public boolean isAbsolute() { - return path.startsWith("/"); + return absolute; } @Override public Path getRoot() { try { - return new JcrPath(filesSystem, node.getSession().getRootNode()); + if (path == null) + return this; + return new JcrPath(fs, fs.getSession().getRootNode()); } catch (RepositoryException e) { throw new JcrFsException("Cannot get root", e); } } + @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(); + } + @Override public Path getFileName() { - return null; + if (path == null) + return null; + return new JcrPath(fs, path[path.length - 1]); } @Override public Path getParent() { - // TODO Auto-generated method stub - return null; + if (path == null) + return null; + if (path.length == 1)// root + return new JcrPath(fs, delimStr); + String[] parentPath = Arrays.copyOfRange(path, 0, path.length - 1); + return new JcrPath(fs, parentPath, absolute); } @Override public int getNameCount() { - // TODO Auto-generated method stub - return 0; + if (path == null) + return 0; + return path.length; } @Override public Path getName(int index) { - // TODO Auto-generated method stub - return null; + if (path == null) + return null; + return new JcrPath(fs, path[index]); } @Override public Path subpath(int beginIndex, int endIndex) { - // TODO Auto-generated method stub - return null; + if (path == null) + return null; + String[] parentPath = Arrays.copyOfRange(path, beginIndex, endIndex); + return new JcrPath(fs, parentPath, false); } @Override public boolean startsWith(Path other) { - // TODO Auto-generated method stub - return false; + return toString().startsWith(other.toString()); } @Override public boolean startsWith(String other) { - // TODO Auto-generated method stub - return false; + return toString().startsWith(other); } @Override public boolean endsWith(Path other) { - // TODO Auto-generated method stub - return false; + return toString().endsWith(other.toString()); } @Override public boolean endsWith(String other) { - // TODO Auto-generated method stub - return false; + return toString().endsWith(other); } @Override public Path normalize() { - // TODO Auto-generated method stub - return null; + // always normalized + return this; } @Override public Path resolve(Path other) { - // TODO Auto-generated method stub - return null; + 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); + } + return new JcrPath(fs, newPath, absolute); } @Override - public Path resolve(String other) { - // TODO Auto-generated method stub - return null; + public final Path resolve(String other) { + return resolve(getFileSystem().getPath(other)); } @Override - public Path resolveSibling(Path other) { - // TODO Auto-generated method stub - return null; + public final Path resolveSibling(Path other) { + if (other == null) + throw new NullPointerException(); + Path parent = getParent(); + return (parent == null) ? other : parent.resolve(other); } @Override - public Path resolveSibling(String other) { - // TODO Auto-generated method stub - return null; + public final Path resolveSibling(String other) { + return resolveSibling(getFileSystem().getPath(other)); + } + + @Override + public final Iterator iterator() { + return new Iterator() { + private int i = 0; + + @Override + public boolean hasNext() { + return (i < getNameCount()); + } + + @Override + public Path next() { + if (i < getNameCount()) { + Path result = getName(i); + i++; + return result; + } else { + throw new NoSuchElementException(); + } + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + }; } @Override public Path relativize(Path other) { - // TODO Auto-generated method stub - return null; + if (equals(other)) + return new JcrPath(fs, ""); + if (other.startsWith(this)) { + String p1 = toString(); + String p2 = other.toString(); + return new JcrPath(fs, p2.substring(p1.length(), p2.length())); + } + throw new UnsupportedOperationException(); } @Override public URI toUri() { - // TODO Auto-generated method stub - return null; + try { + return new URI("jcr", toString(), null); + } catch (URISyntaxException e) { + throw new JcrFsException("Cannot create URI for " + toString(), e); + } } @Override public Path toAbsolutePath() { - // TODO Auto-generated method stub - return null; + if (isAbsolute()) + return this; + return new JcrPath(fs, path, true); } @Override public Path toRealPath(LinkOption... options) throws IOException { - // TODO Auto-generated method stub - return null; + return this; } @Override @@ -159,41 +264,60 @@ public class JcrPath implements Path { } @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 { + public WatchKey register(WatchService watcher, Kind[] events, Modifier... modifiers) throws IOException { // TODO Auto-generated method stub return null; } @Override - public Iterator iterator() { + public WatchKey register(WatchService watcher, Kind... events) throws IOException { // TODO Auto-generated method stub return null; } @Override public int compareTo(Path other) { - // TODO Auto-generated method stub - return 0; + return toString().compareTo(other.toString()); } - public Node getNode() { + public Node getNode() throws RepositoryException { if (!isAbsolute())// TODO default dir throw new JcrFsException("Cannot get node from relative path"); - try { - if (node == null) - node = filesSystem.getSession().getNode(path); - return node; - } catch (RepositoryException e) { - throw new JcrFsException("Cannot get node", e); + String pathStr = toString(); + 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.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 new file mode 100644 index 000000000..dbf674536 --- /dev/null +++ b/org.argeo.jcr/src/org/argeo/jcr/fs/NodeDirectoryStream.java @@ -0,0 +1,63 @@ +package org.argeo.jcr.fs; + +import java.io.IOException; +import java.nio.file.DirectoryStream; +import java.nio.file.Path; +import java.util.Iterator; + +import javax.jcr.Node; +import javax.jcr.NodeIterator; + +public class NodeDirectoryStream implements DirectoryStream { + private final JcrFileSystem fs; + private final NodeIterator nodeIterator; + private final Filter filter; + + public NodeDirectoryStream(JcrFileSystem fs, NodeIterator nodeIterator, Filter filter) { + this.fs = fs; + this.nodeIterator = nodeIterator; + this.filter = filter; + } + + @Override + public void close() throws IOException { + } + + @Override + public Iterator iterator() { + return new Iterator() { + private JcrPath next = null; + + @Override + public synchronized boolean hasNext() { + if (next != null) + return true; + nodes: while (nodeIterator.hasNext()) { + try { + Node node = nodeIterator.nextNode(); + 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); + } + } + 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 new file mode 100644 index 000000000..8054d52f8 --- /dev/null +++ b/org.argeo.jcr/src/org/argeo/jcr/fs/NodeFileAttributes.java @@ -0,0 +1,9 @@ +package org.argeo.jcr.fs; + +import java.nio.file.attribute.BasicFileAttributes; + +import javax.jcr.Node; + +public interface NodeFileAttributes extends BasicFileAttributes { + public Node getNode(); +} diff --git a/org.argeo.jcr/src/org/argeo/jcr/fs/SessionFsProvider.java b/org.argeo.jcr/src/org/argeo/jcr/fs/SessionFsProvider.java new file mode 100644 index 000000000..3c5bf163a --- /dev/null +++ b/org.argeo.jcr/src/org/argeo/jcr/fs/SessionFsProvider.java @@ -0,0 +1,58 @@ +package org.argeo.jcr.fs; + +import java.io.IOException; +import java.net.URI; +import java.nio.file.FileSystem; +import java.nio.file.FileSystemAlreadyExistsException; +import java.nio.file.Path; +import java.util.Map; + +import javax.jcr.Session; + +/** An FS provider based on a single JCR session (experimental). */ +public class SessionFsProvider extends JcrFileSystemProvider { + private Session session; + private JcrFileSystem fileSystem; + + public SessionFsProvider(Session session) { + this.session = session; + } + + @Override + public String getScheme() { + return "jcr+session"; + } + + @Override + public FileSystem newFileSystem(URI uri, Map env) throws IOException { + if (fileSystem != null && fileSystem.isOpen()) + throw new FileSystemAlreadyExistsException(); + fileSystem = new JcrFileSystem(this, session) { + boolean open; + + @Override + public void close() throws IOException { + // prevent the session logout + open = false; + } + + @Override + public boolean isOpen() { + return open; + } + + }; + return fileSystem; + } + + @Override + public FileSystem getFileSystem(URI uri) { + return fileSystem; + } + + @Override + public Path getPath(URI uri) { + return new JcrPath(fileSystem, uri.getPath()); + } + +} 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..bdefff3e4 --- /dev/null +++ b/org.argeo.jcr/src/org/argeo/jcr/fs/WorkSpaceFileStore.java @@ -0,0 +1,67 @@ +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); + } + +}