package org.argeo.jcr.fs; import java.io.IOException; import java.nio.channels.SeekableByteChannel; import java.nio.file.AccessMode; import java.nio.file.CopyOption; import java.nio.file.DirectoryNotEmptyException; import java.nio.file.DirectoryStream; import java.nio.file.DirectoryStream.Filter; import java.nio.file.FileStore; import java.nio.file.LinkOption; import java.nio.file.NoSuchFileException; import java.nio.file.OpenOption; import java.nio.file.Path; import java.nio.file.attribute.BasicFileAttributes; import java.nio.file.attribute.FileAttribute; import java.nio.file.attribute.FileAttributeView; import java.nio.file.spi.FileSystemProvider; import java.util.Calendar; import java.util.HashMap; import java.util.Map; import java.util.Set; import javax.jcr.Node; import javax.jcr.Property; import javax.jcr.PropertyIterator; import javax.jcr.PropertyType; import javax.jcr.Repository; import javax.jcr.RepositoryException; import javax.jcr.Session; import javax.jcr.nodetype.NodeType; import javax.jcr.nodetype.PropertyDefinition; import org.argeo.jcr.JcrUtils; /** Operations on a {@link JcrFileSystem}. */ public abstract class JcrFileSystemProvider extends FileSystemProvider { @Override public SeekableByteChannel newByteChannel(Path path, Set options, FileAttribute... attrs) throws IOException { Node node = toNode(path); try { if (node == null) { Node parent = toNode(path.getParent()); if (parent == null) throw new IOException("No parent directory for " + path); if (parent.getPrimaryNodeType().isNodeType(NodeType.NT_FILE) || parent.getPrimaryNodeType().isNodeType(NodeType.NT_LINKED_FILE)) throw new IOException(path + " parent is a file"); String fileName = path.getFileName().toString(); fileName = Text.escapeIllegalJcrChars(fileName); node = parent.addNode(fileName, NodeType.NT_FILE); node.addMixin(NodeType.MIX_CREATED); // node.addMixin(NodeType.MIX_LAST_MODIFIED); } if (!node.isNodeType(NodeType.NT_FILE)) throw new UnsupportedOperationException(node + " must be a file"); return new BinaryChannel(node, path); } catch (RepositoryException e) { discardChanges(node); throw new IOException("Cannot read file", e); } } @Override public DirectoryStream newDirectoryStream(Path dir, Filter filter) throws IOException { try { Node base = toNode(dir); if (base == null) throw new IOException(dir + " is not a JCR node"); JcrFileSystem fileSystem = (JcrFileSystem) dir.getFileSystem(); return new NodeDirectoryStream(fileSystem, base.getNodes(), fileSystem.listDirectMounts(dir), filter); } catch (RepositoryException e) { throw new IOException("Cannot list directory", e); } } @Override public void createDirectory(Path dir, FileAttribute... attrs) throws IOException { Node node = toNode(dir); try { if (node == null) { Node parent = toNode(dir.getParent()); if (parent == null) throw new IOException("Parent of " + dir + " does not exist"); Session session = parent.getSession(); synchronized (session) { if (parent.getPrimaryNodeType().isNodeType(NodeType.NT_FILE) || parent.getPrimaryNodeType().isNodeType(NodeType.NT_LINKED_FILE)) throw new IOException(dir + " parent is a file"); String fileName = dir.getFileName().toString(); fileName = Text.escapeIllegalJcrChars(fileName); node = parent.addNode(fileName, NodeType.NT_FOLDER); node.addMixin(NodeType.MIX_CREATED); node.addMixin(NodeType.MIX_LAST_MODIFIED); save(session); } } else { // if (!node.getPrimaryNodeType().isNodeType(NodeType.NT_FOLDER)) // throw new FileExistsException(dir + " exists and is not a directory"); } } catch (RepositoryException e) { discardChanges(node); throw new IOException("Cannot create directory " + dir, e); } } @Override public void delete(Path path) throws IOException { Node node = toNode(path); try { if (node == null) throw new NoSuchFileException(path + " does not exist"); Session session = node.getSession(); synchronized (session) { session.refresh(false); if (node.getPrimaryNodeType().isNodeType(NodeType.NT_FILE)) node.remove(); else if (node.getPrimaryNodeType().isNodeType(NodeType.NT_FOLDER)) { if (node.hasNodes())// TODO check only files throw new DirectoryNotEmptyException(path.toString()); node.remove(); } save(session); } } catch (RepositoryException e) { discardChanges(node); throw new IOException("Cannot delete " + path, e); } } @Override public void copy(Path source, Path target, CopyOption... options) throws IOException { Node sourceNode = toNode(source); Node targetNode = toNode(target); try { Session targetSession = targetNode.getSession(); synchronized (targetSession) { JcrUtils.copy(sourceNode, targetNode); save(targetSession); } } catch (RepositoryException e) { discardChanges(sourceNode); discardChanges(targetNode); throw new IOException("Cannot copy from " + source + " to " + target, e); } } @Override public void move(Path source, Path target, CopyOption... options) throws IOException { JcrFileSystem sourceFileSystem = (JcrFileSystem) source.getFileSystem(); WorkspaceFileStore sourceStore = sourceFileSystem.getFileStore(source.toString()); WorkspaceFileStore targetStore = sourceFileSystem.getFileStore(target.toString()); try { if (sourceStore.equals(targetStore)) { sourceStore.getWorkspace().move(sourceStore.toJcrPath(source.toString()), targetStore.toJcrPath(target.toString())); } else { // TODO implement it throw new UnsupportedOperationException("Can only move paths within the same workspace."); } } catch (RepositoryException e) { throw new IOException("Cannot move from " + source + " to " + target, e); } // Node sourceNode = toNode(source); // try { // Session session = sourceNode.getSession(); // synchronized (session) { // session.move(sourceNode.getPath(), target.toString()); // save(session); // } // } catch (RepositoryException e) { // discardChanges(sourceNode); // throw new IOException("Cannot move from " + source + " to " + target, e); // } } @Override public boolean isSameFile(Path path, Path path2) throws IOException { if (path.getFileSystem() != path2.getFileSystem()) return false; boolean equ = path.equals(path2); if (equ) return true; else { try { Node node = toNode(path); Node node2 = toNode(path2); return node.isSame(node2); } catch (RepositoryException e) { throw new IOException("Cannot check whether " + path + " and " + path2 + " are same", e); } } } @Override public boolean isHidden(Path path) throws IOException { return path.getFileName().toString().charAt(0) == '.'; } @Override public FileStore getFileStore(Path path) throws IOException { JcrFileSystem fileSystem = (JcrFileSystem) path.getFileSystem(); return fileSystem.getFileStore(path.toString()); } @Override public void checkAccess(Path path, AccessMode... modes) throws IOException { Node node = toNode(path); if (node == null) throw new NoSuchFileException(path + " does not exist"); // TODO check access via JCR api } @Override public V getFileAttributeView(Path path, Class type, LinkOption... options) { throw new UnsupportedOperationException(); } @SuppressWarnings("unchecked") @Override public A readAttributes(Path path, Class type, LinkOption... options) throws IOException { // TODO check if assignable Node node = toNode(path); if (node == null) { throw new IOException("JCR node not found for " + path); } return (A) new JcrBasicfileAttributes(node); } @Override public Map readAttributes(Path path, String attributes, LinkOption... options) throws IOException { try { Node node = toNode(path); String pattern = attributes.replace(',', '|'); Map res = new HashMap(); PropertyIterator it = node.getProperties(pattern); props: while (it.hasNext()) { Property prop = it.nextProperty(); PropertyDefinition pd = prop.getDefinition(); if (pd.isMultiple()) continue props; int requiredType = pd.getRequiredType(); switch (requiredType) { case PropertyType.LONG: res.put(prop.getName(), prop.getLong()); break; case PropertyType.DOUBLE: res.put(prop.getName(), prop.getDouble()); break; case PropertyType.BOOLEAN: res.put(prop.getName(), prop.getBoolean()); break; case PropertyType.DATE: res.put(prop.getName(), prop.getDate()); break; case PropertyType.BINARY: byte[] arr = JcrUtils.getBinaryAsBytes(prop); res.put(prop.getName(), arr); break; default: res.put(prop.getName(), prop.getString()); } } return res; } catch (RepositoryException e) { throw new IOException("Cannot read attributes of " + path, e); } } @Override public void setAttribute(Path path, String attribute, Object value, LinkOption... options) throws IOException { Node node = toNode(path); try { Session session = node.getSession(); synchronized (session) { if (value instanceof byte[]) { JcrUtils.setBinaryAsBytes(node, attribute, (byte[]) value); } else if (value instanceof Calendar) { node.setProperty(attribute, (Calendar) value); } else { node.setProperty(attribute, value.toString()); } save(session); } } catch (RepositoryException e) { discardChanges(node); throw new IOException("Cannot set attribute " + attribute + " on " + path, e); } } protected Node toNode(Path path) { try { return ((JcrPath) path).getNode(); } catch (RepositoryException e) { throw new JcrFsException("Cannot convert path " + path + " to JCR Node", e); } } /** Discard changes in the underlying session */ protected void discardChanges(Node node) { if (node == null) return; try { // discard changes node.getSession().refresh(false); } catch (RepositoryException e) { e.printStackTrace(); // TODO log out session? // TODO use Commons logging? } } /** Make sure save is robust. */ protected void save(Session session) throws RepositoryException { session.refresh(true); session.save(); session.notifyAll(); } /** * To be overriden in order to support the ~ path, with an implementation * specific concept of user home. * * @return null by default */ public Node getUserHome(Repository session) { return null; } }