Index: extensions/modeshape-connector-filesystem/src/main/java/org/modeshape/connector/filesystem/FileSystemI18n.java =================================================================== --- extensions/modeshape-connector-filesystem/src/main/java/org/modeshape/connector/filesystem/FileSystemI18n.java (revision 1823) +++ extensions/modeshape-connector-filesystem/src/main/java/org/modeshape/connector/filesystem/FileSystemI18n.java (working copy) @@ -38,7 +38,6 @@ public final class FileSystemI18n { public static I18n pathForWorkspaceRootIsNotDirectory; public static I18n pathForWorkspaceRootCannotBeRead; public static I18n propertyIsRequired; - public static I18n locationInRequestMustHavePath; public static I18n sameNameSiblingsAreNotAllowed; public static I18n nodeOrderingNotSupported; public static I18n onlyTheDefaultNamespaceIsAllowed; @@ -63,7 +62,6 @@ public final class FileSystemI18n { public static I18n couldNotUpdateData; public static I18n missingRequiredProperty; public static I18n deleteFailed; - public static I18n copyFailed; public static I18n getCanonicalPathFailed; public static I18n maxPathLengthExceeded; Index: extensions/modeshape-connector-filesystem/src/main/java/org/modeshape/connector/filesystem/FileSystemRepository.java =================================================================== --- extensions/modeshape-connector-filesystem/src/main/java/org/modeshape/connector/filesystem/FileSystemRepository.java (revision 1823) +++ extensions/modeshape-connector-filesystem/src/main/java/org/modeshape/connector/filesystem/FileSystemRepository.java (working copy) @@ -23,60 +23,35 @@ */ package org.modeshape.connector.filesystem; -import java.io.BufferedInputStream; import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; +import java.util.LinkedList; +import java.util.UUID; import org.modeshape.common.i18n.I18n; -import org.modeshape.common.util.FileUtil; -import org.modeshape.common.util.IoUtil; import org.modeshape.graph.ExecutionContext; -import org.modeshape.graph.JcrLexicon; import org.modeshape.graph.JcrNtLexicon; -import org.modeshape.graph.Location; import org.modeshape.graph.ModeShapeLexicon; -import org.modeshape.graph.NodeConflictBehavior; -import org.modeshape.graph.connector.RepositorySourceException; -import org.modeshape.graph.connector.path.AbstractWritablePathWorkspace; -import org.modeshape.graph.connector.path.DefaultPathNode; -import org.modeshape.graph.connector.path.PathNode; -import org.modeshape.graph.connector.path.WritablePathRepository; -import org.modeshape.graph.connector.path.WritablePathWorkspace; -import org.modeshape.graph.connector.path.cache.WorkspaceCache; -import org.modeshape.graph.mimetype.MimeTypeDetector; -import org.modeshape.graph.property.Binary; -import org.modeshape.graph.property.BinaryFactory; -import org.modeshape.graph.property.DateTimeFactory; -import org.modeshape.graph.property.Name; -import org.modeshape.graph.property.NameFactory; -import org.modeshape.graph.property.NamespaceRegistry; +import org.modeshape.graph.connector.RepositoryContext; +import org.modeshape.graph.connector.base.PathNode; +import org.modeshape.graph.connector.base.PathTransaction; +import org.modeshape.graph.connector.base.Processor; +import org.modeshape.graph.connector.base.Repository; +import org.modeshape.graph.connector.base.Transaction; +import org.modeshape.graph.observe.Observer; import org.modeshape.graph.property.Path; -import org.modeshape.graph.property.PathFactory; -import org.modeshape.graph.property.PathNotFoundException; import org.modeshape.graph.property.Property; -import org.modeshape.graph.property.PropertyFactory; import org.modeshape.graph.property.Path.Segment; import org.modeshape.graph.request.InvalidRequestException; import org.modeshape.graph.request.InvalidWorkspaceException; -import org.modeshape.graph.request.Request; +import org.modeshape.graph.request.MoveBranchRequest; +import org.modeshape.graph.request.processor.RequestProcessor; /** - * Implementation of {@code WritablePathRepository} that provides access to an underlying file system. This repository only - * natively supports nodes of primary types {@link JcrNtLexicon#FOLDER nt:folder}, {@link JcrNtLexicon#FILE nt:file}, and + * Implementation of {@code Repository} that provides access to an underlying file system. This repository only natively supports + * nodes of primary types {@link JcrNtLexicon#FOLDER nt:folder}, {@link JcrNtLexicon#FILE nt:file}, and * {@link ModeShapeLexicon#RESOURCE mode:resource}, although the {@link CustomPropertiesFactory} allows for the addition of mixin * types to any and all primary types. */ -public class FileSystemRepository extends WritablePathRepository { - private static final String DEFAULT_MIME_TYPE = "application/octet"; +public class FileSystemRepository extends Repository { protected final FileSystemSource source; private File repositoryRoot; @@ -112,51 +87,8 @@ public class FileSystemRepository extends WritablePathRepository { } } - if (!this.workspaces.isEmpty()) return; - - String defaultWorkspaceName = getDefaultWorkspaceName(); - ExecutionContext context = source.getRepositoryContext().getExecutionContext(); - - for (String workspaceName : source.getPredefinedWorkspaceNames()) { - doCreateWorkspace(context, workspaceName); - - } - - if (!workspaces.containsKey(defaultWorkspaceName)) { - doCreateWorkspace(context, defaultWorkspaceName); - } - } - - public WorkspaceCache getCache( String workspaceName ) { - return source.getPathRepositoryCache().getCache(workspaceName); - } - - /** - * Internal method that creates a workspace and adds it to the map of active workspaces without checking to see if - * {@link FileSystemSource#isCreatingWorkspacesAllowed() the source allows creating workspaces}. This is useful when setting - * up predefined workspaces. - * - * @param context the current execution context; may not be null - * @param name the name of the workspace to create; may not be null - * @return the newly created workspace; never null - */ - private WritablePathWorkspace doCreateWorkspace( ExecutionContext context, - String name ) { - File directory = getWorkspaceDirectory(name); - - FileSystemWorkspace workspace = new FileSystemWorkspace(name, context, directory); - - workspaces.putIfAbsent(name, workspace); - return (WritablePathWorkspace)workspaces.get(name); - + super.initialize(); } - - /** - * Creates directory and any missing parent directories - * - * @param directory the directory to create - * @throws RepositorySourceException if the directory or one of its parents cannot be created - */ private void createDirectory( File directory ) { File parent = directory.getParentFile(); @@ -173,17 +105,6 @@ public class FileSystemRepository extends WritablePathRepository { } } - @Override - protected WritablePathWorkspace createWorkspace( ExecutionContext context, - String name ) { - if (!source.isCreatingWorkspacesAllowed()) { - String msg = FileSystemI18n.unableToCreateWorkspaces.text(getSourceName(), name); - throw new InvalidRequestException(msg); - } - - return doCreateWorkspace(context, name); - } - /** * @param workspaceName the name of the workspace for which the root directory should be returned * @return the directory that maps to the root node in the named workspace; may be null if the directory does not exist, is a @@ -210,520 +131,79 @@ public class FileSystemRepository extends WritablePathRepository { return directory; } + @Override + public FileSystemTransaction startTransaction( ExecutionContext context, + boolean readonly ) { + return new FileSystemTransaction(this, source.getRootNodeUuidObject()); + } + + @Override + public RequestProcessor createRequestProcessor( Transaction txn ) { + RepositoryContext repositoryContext = this.source.getRepositoryContext(); + Observer observer = repositoryContext != null ? repositoryContext.getObserver() : null; + return new FileSystemProcessor(txn, this, observer, source.areUpdatesAllowed()); + } + /** - * Writable workspace implementation for file system-backed workspaces + * Implementation of the {@link PathTransaction} interface for the file system connector */ - public class FileSystemWorkspace extends AbstractWritablePathWorkspace { - - private final ExecutionContext context; - private final File workspaceRoot; + class FileSystemTransaction extends PathTransaction { - public FileSystemWorkspace( String name, - ExecutionContext context, - File workspaceRoot ) { - super(name, source.getRootNodeUuid()); - this.workspaceRoot = workspaceRoot; - this.context = context; + public FileSystemTransaction( FileSystemRepository repository, + UUID rootNodeUuid ) { + super(repository, rootNodeUuid); } - public PathNode createNode( ExecutionContext context, - PathNode parentNode, - Name name, - Map properties, - NodeConflictBehavior conflictBehavior ) { - NameFactory nameFactory = context.getValueFactories().getNameFactory(); - PathFactory pathFactory = context.getValueFactories().getPathFactory(); - NamespaceRegistry registry = context.getNamespaceRegistry(); - /* - * Get references to java.io.Files - */ - - Path parentPath = parentNode.getPath(); - File parentFile = fileFor(parentPath); - - Path newPath = pathFactory.create(parentPath, name); - String newName = name.getString(registry); - File newFile = new File(parentFile, newName); - - /* - * Determine the node primary type - */ - Property primaryTypeProp = properties.get(JcrLexicon.PRIMARY_TYPE); - - // Default primary type to nt:folder - Name primaryType = primaryTypeProp == null ? JcrNtLexicon.FOLDER : nameFactory.create(primaryTypeProp.getFirstValue()); - CustomPropertiesFactory customPropertiesFactory = source.customPropertiesFactory(); - - if (JcrNtLexicon.FILE.equals(primaryType)) { - - // The FILE node is represented by the existence of the file - if (!parentFile.canWrite()) { - I18n msg = FileSystemI18n.parentIsReadOnly; - throw new RepositorySourceException(getSourceName(), msg.text(parentPath, this.getName(), getSourceName())); - } - - try { - ensureValidPathLength(newFile); - boolean skipWrite = false; - - if (newFile.exists()) { - if (conflictBehavior.equals(NodeConflictBehavior.APPEND)) { - I18n msg = FileSystemI18n.sameNameSiblingsAreNotAllowed; - throw new InvalidRequestException(msg.text(getSourceName(), newName)); - } else if (conflictBehavior.equals(NodeConflictBehavior.DO_NOT_REPLACE)) { - skipWrite = true; - } - } - - // Don't try to write if the node conflict behavior is DO_NOT_REPLACE - if (!skipWrite) { - if (!newFile.createNewFile()) { - I18n msg = FileSystemI18n.fileAlreadyExists; - throw new RepositorySourceException(getSourceName(), msg.text(parentPath, getName(), getSourceName())); - } - } - } catch (IOException ioe) { - I18n msg = FileSystemI18n.couldNotCreateFile; - throw new RepositorySourceException(getSourceName(), msg.text(parentPath, - getName(), - getSourceName(), - ioe.getMessage()), ioe); - } - - customPropertiesFactory.recordFileProperties(context, - getSourceName(), - Location.create(newPath), - newFile, - properties); - } else if (JcrNtLexicon.RESOURCE.equals(primaryType) || ModeShapeLexicon.RESOURCE.equals(primaryType)) { - if (!JcrLexicon.CONTENT.equals(name)) { - I18n msg = FileSystemI18n.invalidNameForResource; - String nodeName = name.getString(); - throw new RepositorySourceException(getSourceName(), msg.text(parentPath, - getName(), - getSourceName(), - nodeName)); - } - - if (!parentFile.isFile()) { - I18n msg = FileSystemI18n.invalidPathForResource; - throw new RepositorySourceException(getSourceName(), msg.text(parentPath, getName(), getSourceName())); - } - - if (!parentFile.canWrite()) { - I18n msg = FileSystemI18n.parentIsReadOnly; - throw new RepositorySourceException(getSourceName(), msg.text(parentPath, getName(), getSourceName())); - } - - boolean updateFileContent = true; - switch (conflictBehavior) { - case APPEND: - case REPLACE: - case UPDATE: - // When the "nt:file" parent node was created, it automatically creates the "jcr:content" - // child node with empty content. Therefore, creating a new "jcr:content" node is - // not technically possible (recall that same-name-siblings are not supported in general, - // but certainly not for the "jcr:content" node). Therefore, we can treat all these - // conflict behavior cases as a simple update to the existing "jcr:content" child node. - break; - case DO_NOT_REPLACE: - updateFileContent = false; - } - - if (updateFileContent) { - // Copy over data into a temp file, then move it to the correct location - FileOutputStream fos = null; - try { - File temp = File.createTempFile("dna", null); - fos = new FileOutputStream(temp); - - Property dataProp = properties.get(JcrLexicon.DATA); - if (dataProp == null) { - I18n msg = FileSystemI18n.missingRequiredProperty; - String dataPropName = JcrLexicon.DATA.getString(); - throw new RepositorySourceException(getSourceName(), msg.text(parentPath, - getName(), - getSourceName(), - dataPropName)); - } - - BinaryFactory binaryFactory = context.getValueFactories().getBinaryFactory(); - Binary binary = binaryFactory.create(properties.get(JcrLexicon.DATA).getFirstValue()); - - IoUtil.write(binary.getStream(), fos); - - if (!FileUtil.delete(parentFile)) { - I18n msg = FileSystemI18n.deleteFailed; - throw new RepositorySourceException(getSourceName(), msg.text(parentPath, getName(), getSourceName())); - } - - if (!temp.renameTo(parentFile)) { - I18n msg = FileSystemI18n.couldNotUpdateData; - throw new RepositorySourceException(getSourceName(), msg.text(parentPath, getName(), getSourceName())); - } - } catch (IOException ioe) { - I18n msg = FileSystemI18n.couldNotWriteData; - throw new RepositorySourceException(getSourceName(), msg.text(parentPath, - getName(), - getSourceName(), - ioe.getMessage()), ioe); - - } finally { - try { - if (fos != null) fos.close(); - } catch (Exception ex) { - } - } - } - customPropertiesFactory.recordResourceProperties(context, - getSourceName(), - Location.create(parentPath), - newFile, - properties); - - } else if (JcrNtLexicon.FOLDER.equals(primaryType) || primaryType == null) { - ensureValidPathLength(newFile); - - if (!newFile.mkdir()) { - I18n msg = FileSystemI18n.couldNotCreateFile; - throw new RepositorySourceException(getSourceName(), - msg.text(parentPath, - getName(), - getSourceName(), - primaryType == null ? "null" : primaryType.getString(registry))); - } - customPropertiesFactory.recordDirectoryProperties(context, - getSourceName(), - Location.create(newPath), - newFile, - properties); - - } else { - // Set error and return - I18n msg = FileSystemI18n.unsupportedPrimaryType; - throw new RepositorySourceException(getSourceName(), msg.text(primaryType.getString(registry), - parentPath, - getName(), - getSourceName())); - } - - PathNode node = getNode(newPath); - - List newChildren = new ArrayList(parentNode.getChildSegments().size() + 1); - newChildren.addAll(parentNode.getChildSegments()); - newChildren.add(node.getPath().getLastSegment()); - - WorkspaceCache cache = getCache(getName()); - cache.set(new DefaultPathNode(parentNode.getPath(), parentNode.getUuid(), parentNode.getProperties(), newChildren)); - cache.set(node); - - return node; + @Override + protected PathNode createNode( Segment name, + Path parentPath, + Iterable properties ) { + return new PathNode(null, parentPath, name, properties, new LinkedList()); } - public boolean removeNode( ExecutionContext context, - Path nodePath ) { - File nodeFile; - - if (!nodePath.isRoot() && JcrLexicon.CONTENT.equals(nodePath.getLastSegment().getName())) { - nodeFile = fileFor(nodePath.getParent()); - if (!nodeFile.exists()) return false; - - FileOutputStream fos = null; - try { - fos = new FileOutputStream(nodeFile); - IoUtil.write("", fos); - } catch (IOException ioe) { - throw new RepositorySourceException(getSourceName(), FileSystemI18n.deleteFailed.text(nodePath, - getName(), - getSourceName())); - } finally { - if (fos != null) try { - fos.close(); - } catch (IOException ioe) { - } - } - } else { - nodeFile = fileFor(nodePath); - if (!nodeFile.exists()) return false; - - FileUtil.delete(nodeFile); - } - + @Override + public boolean destroyWorkspace( FileSystemWorkspace workspace ) throws InvalidWorkspaceException { return true; } - public PathNode setProperties( ExecutionContext context, - Path nodePath, - Map properties ) { - PathNode targetNode = getNode(nodePath); - if (targetNode == null) return null; - if (source.getCustomPropertiesFactory() == null) return targetNode; - - Property primaryTypeProp = targetNode.getProperty(JcrLexicon.PRIMARY_TYPE); - Name primaryTypeName = (Name)primaryTypeProp.getFirstValue(); - - CustomPropertiesFactory customPropertiesFactory = source.customPropertiesFactory(); - Location location = Location.create(nodePath, targetNode.getUuid()); - - /* - * You can't remove any of the protected properties that the repository provides by default, but you could - * remove custom properties. - */ - if (JcrNtLexicon.FILE.equals(primaryTypeName)) { - customPropertiesFactory.recordFileProperties(context, getSourceName(), location, fileFor(nodePath), properties); - } else if (ModeShapeLexicon.RESOURCE.equals(primaryTypeName)) { - File file = fileFor(nodePath.getParent()); - customPropertiesFactory.recordResourceProperties(context, getSourceName(), location, file, properties); - } else { - File file = fileFor(nodePath); - customPropertiesFactory.recordDirectoryProperties(context, getSourceName(), location, file, properties); - } - - PathNode node = getNode(nodePath); - getCache(getName()).set(node); - - return node; - } - @Override - public PathNode moveNode( ExecutionContext context, - PathNode node, - Name desiredNewName, - WritablePathWorkspace originalWorkspace, - PathNode newParent, - PathNode beforeNode ) { - if (beforeNode != null) { - throw new InvalidRequestException(FileSystemI18n.nodeOrderingNotSupported.text(getSourceName())); - } - PathNode movedNode = super.moveNode(context, node, desiredNewName, originalWorkspace, newParent, beforeNode); - - getCache(getName()).invalidate(node.getPath()); + public FileSystemWorkspace getWorkspace( String name, + FileSystemWorkspace originalToClone ) throws InvalidWorkspaceException { + FileSystemRepository repository = FileSystemRepository.this; - return movedNode; - } - - public Path getLowestExistingPath( Path path ) { - File file = workspaceRoot; - for (Path.Segment segment : path) { - String localName = segment.getName().getLocalName(); - // Verify the segment is valid ... - if (segment.getIndex() > 1) { - break; - } - - String defaultNamespaceUri = context.getNamespaceRegistry().getDefaultNamespaceUri(); - if (!segment.getName().getNamespaceUri().equals(defaultNamespaceUri)) { - break; - } - - // The segment should exist as a child of the file ... - file = new File(file, localName); - if (!file.exists() || !file.canRead()) { - // Unable to complete the path, so prepare the exception by determining the lowest path that exists ... - Path lowest = path; - while (lowest.getLastSegment() != segment) { - lowest = lowest.getParent(); - } - return lowest.getParent(); - } + if (originalToClone != null) { + return new FileSystemWorkspace(name, originalToClone, repository.getWorkspaceDirectory(name)); } - // Shouldn't be able to get this far is path is truly invalid - return path; + return new FileSystemWorkspace(repository, name); } - public PathNode getNode( Path path ) { - WorkspaceCache cache = getCache(getName()); - - PathNode node = cache.get(path); - if (node != null) return node; - - Map properties = new HashMap(); - - PropertyFactory factory = context.getPropertyFactory(); - PathFactory pathFactory = context.getValueFactories().getPathFactory(); - DateTimeFactory dateFactory = context.getValueFactories().getDateFactory(); - MimeTypeDetector mimeTypeDetector = context.getMimeTypeDetector(); - CustomPropertiesFactory customPropertiesFactory = source.customPropertiesFactory(); - NamespaceRegistry registry = context.getNamespaceRegistry(); - Location location = Location.create(path); - - if (!path.isRoot() && JcrLexicon.CONTENT.equals(path.getLastSegment().getName())) { - File file = fileFor(path.getParent()); - if (file == null) return null; - // Discover the mime type ... - String mimeType = null; - InputStream contents = null; - try { - contents = new BufferedInputStream(new FileInputStream(file)); - mimeType = mimeTypeDetector.mimeTypeOf(file.getName(), contents); - if (mimeType == null) mimeType = DEFAULT_MIME_TYPE; - properties.put(JcrLexicon.MIMETYPE, factory.create(JcrLexicon.MIMETYPE, mimeType)); - } catch (IOException e) { - I18n msg = FileSystemI18n.couldNotReadData; - throw new RepositorySourceException(getSourceName(), msg.text(getSourceName(), - getName(), - path.getString(registry))); - } finally { - if (contents != null) { - try { - contents.close(); - } catch (IOException e) { - } - } - } - - // First add any custom properties ... - Collection customProps = customPropertiesFactory.getResourceProperties(context, - location, - file, - mimeType); - for (Property customProp : customProps) { - properties.put(customProp.getName(), customProp); - } - - // The request is to get properties of the "jcr:content" child node ... - // ... use the dna:resource node type. This is the same as nt:resource, but is not referenceable - // since we cannot assume that we control all access to this file and can track its movements - properties.put(JcrLexicon.PRIMARY_TYPE, factory.create(JcrLexicon.PRIMARY_TYPE, ModeShapeLexicon.RESOURCE)); - properties.put(JcrLexicon.LAST_MODIFIED, factory.create(JcrLexicon.LAST_MODIFIED, - dateFactory.create(file.lastModified()))); - // Don't really know the encoding, either ... - // request.addProperty(factory.create(JcrLexicon.ENCODED, stringFactory.create("UTF-8"))); - - // Now put the file's content into the "jcr:data" property ... - BinaryFactory binaryFactory = context.getValueFactories().getBinaryFactory(); - properties.put(JcrLexicon.DATA, factory.create(JcrLexicon.DATA, binaryFactory.create(file))); - return new DefaultPathNode(path, null, properties, Collections.emptyList()); - } - - File file = fileFor(path); - if (file == null) return null; - - if (file.isDirectory()) { - String[] childNames = file.list(source.filenameFilter()); - Arrays.sort(childNames); - - List childSegments = new ArrayList(childNames.length); - for (String childName : childNames) { - childSegments.add(pathFactory.createSegment(childName)); - } - - Collection customProps = customPropertiesFactory.getDirectoryProperties(context, location, file); - for (Property customProp : customProps) { - properties.put(customProp.getName(), customProp); - } - - if (path.isRoot()) { - properties.put(JcrLexicon.PRIMARY_TYPE, factory.create(JcrLexicon.PRIMARY_TYPE, ModeShapeLexicon.ROOT)); - return new DefaultPathNode(path, source.getRootNodeUuid(), properties, childSegments); - } - properties.put(JcrLexicon.PRIMARY_TYPE, factory.create(JcrLexicon.PRIMARY_TYPE, JcrNtLexicon.FOLDER)); - return new DefaultPathNode(path, source.getRootNodeUuid(), properties, childSegments); - } - - Collection customProps = customPropertiesFactory.getFileProperties(context, location, file); - for (Property customProp : customProps) { - properties.put(customProp.getName(), customProp); - } - properties.put(JcrLexicon.PRIMARY_TYPE, factory.create(JcrLexicon.PRIMARY_TYPE, JcrNtLexicon.FILE)); - properties.put(JcrLexicon.CREATED, factory.create(JcrLexicon.CREATED, dateFactory.create(file.lastModified()))); - node = new DefaultPathNode(path, null, properties, - Collections.singletonList(pathFactory.createSegment(JcrLexicon.CONTENT))); - - cache.set(node); - return node; + @Override + protected void validateNode( FileSystemWorkspace workspace, + PathNode node ) { + workspace.validate(node); } + } - /** - * This utility files the existing {@link File} at the supplied path, and in the process will verify that the path is - * actually valid. - *

- * Note that this connector represents a file as two nodes: a parent node with a name that matches the file and a " - * jcr:primaryType" of "nt:file"; and a child node with the name "jcr:content - * " and a " jcr:primaryType" of "nt:resource". The parent "nt:file" node and its - * properties represents the file itself, whereas the child "nt:resource" node and its properties represent - * the content of the file. - *

- *

- * As such, this method will return the File object for paths representing both the parent "nt:file - * " and child " nt:resource" node. - *

- * - * @param path - * @return the existing {@link File file} for the path; or null if the path does not represent an existing file and a - * {@link PathNotFoundException} was set as the {@link Request#setError(Throwable) error} on the request - */ - protected File fileFor( Path path ) { - assert path != null; - if (path.isRoot()) { - return workspaceRoot; - } - // See if the path is a "jcr:content" node ... - if (path.getLastSegment().getName().equals(JcrLexicon.CONTENT)) { - // We only want to use the parent path to find the actual file ... - path = path.getParent(); - } - File file = workspaceRoot; - for (Path.Segment segment : path) { - String localName = segment.getName().getLocalName(); - // Verify the segment is valid ... - if (segment.getIndex() > 1) { - I18n msg = FileSystemI18n.sameNameSiblingsAreNotAllowed; - throw new RepositorySourceException(getSourceName(), msg.text(getSourceName())); - } - - String defaultNamespaceUri = context.getNamespaceRegistry().getDefaultNamespaceUri(); - if (!segment.getName().getNamespaceUri().equals(defaultNamespaceUri)) { - I18n msg = FileSystemI18n.onlyTheDefaultNamespaceIsAllowed; - throw new RepositorySourceException(getSourceName(), msg.text(getSourceName())); - } - - // The segment should exist as a child of the file ... - file = new File(file, localName); - if (!file.exists() || !file.canRead()) { - return null; - } - } - assert file != null; - return file; - } + /** + * Custom {@link Processor} for the file system connector. This processor throws accurate exceptions on attempts to reorder + * nodes, since the file system connector does not support node ordering. Otherwise, it provides default behavior. + */ + class FileSystemProcessor extends Processor { - protected void ensureValidPathLength( File root ) { - ensureValidPathLength(root, 0); + public FileSystemProcessor( Transaction txn, + Repository repository, + Observer observer, + boolean updatesAllowed ) { + super(txn, repository, observer, updatesAllowed); } - /** - * Recursively checks if any of the files in the tree rooted at {@code root} would exceed the - * {@link FileSystemSource#getMaxPathLength() maximum path length for the processor} if their paths were {@code delta} - * characters longer. If any files would exceed this length, a {@link RepositorySourceException} is thrown. - * - * @param root the root of the tree to check; may be a file or directory but may not be null - * @param delta the change in the length of the path to check. Used to preemptively check whether moving a file or - * directory to a new path would violate path length rules - * @throws RepositorySourceException if any files in the tree rooted at {@code root} would exceed this - * {@link FileSystemSource#getMaxPathLength() the maximum path length for this processor} - */ - protected void ensureValidPathLength( File root, - int delta ) { - try { - int len = root.getCanonicalPath().length(); - if (len > source.getMaxPathLength() - delta) { - String msg = FileSystemI18n.maxPathLengthExceeded.text(source.getMaxPathLength(), - getSourceName(), - root.getCanonicalPath(), - delta); - throw new RepositorySourceException(getSourceName(), msg); - } - - if (root.isDirectory()) { - for (File child : root.listFiles(source.filenameFilter())) { - ensureValidPathLength(child, delta); - } - - } - } catch (IOException ioe) { - throw new RepositorySourceException(getSourceName(), FileSystemI18n.getCanonicalPathFailed.text(), ioe); + @Override + public void process( MoveBranchRequest request ) { + if (request.before() != null) { + I18n msg = FileSystemI18n.nodeOrderingNotSupported; + throw new InvalidRequestException(msg.text(source.getName())); } + super.process(request); } } Index: extensions/modeshape-connector-filesystem/src/main/java/org/modeshape/connector/filesystem/FileSystemSource.java =================================================================== --- extensions/modeshape-connector-filesystem/src/main/java/org/modeshape/connector/filesystem/FileSystemSource.java (revision 1823) +++ extensions/modeshape-connector-filesystem/src/main/java/org/modeshape/connector/filesystem/FileSystemSource.java (working copy) @@ -44,6 +44,7 @@ import net.jcip.annotations.ThreadSafe; import org.modeshape.common.i18n.I18n; import org.modeshape.common.util.CheckArg; import org.modeshape.common.util.StringUtil; +import org.modeshape.connector.filesystem.FileSystemRepository.FileSystemTransaction; import org.modeshape.graph.ExecutionContext; import org.modeshape.graph.JcrLexicon; import org.modeshape.graph.Location; @@ -52,11 +53,13 @@ import org.modeshape.graph.connector.RepositoryConnection; import org.modeshape.graph.connector.RepositorySource; import org.modeshape.graph.connector.RepositorySourceCapabilities; import org.modeshape.graph.connector.RepositorySourceException; -import org.modeshape.graph.connector.path.AbstractPathRepositorySource; -import org.modeshape.graph.connector.path.PathRepositoryConnection; +import org.modeshape.graph.connector.base.AbstractRepositorySource; +import org.modeshape.graph.connector.base.Connection; +import org.modeshape.graph.connector.base.PathNode; import org.modeshape.graph.property.Name; import org.modeshape.graph.property.NamespaceRegistry; import org.modeshape.graph.property.Property; +import org.modeshape.graph.request.CreateWorkspaceRequest.CreateConflictBehavior; /** * The {@link RepositorySource} for the connector that exposes an area of the local file system as content in a repository. This @@ -64,7 +67,7 @@ import org.modeshape.graph.property.Property; * workspace. New workspaces can be created, as long as the names represent valid paths to existing directories. */ @ThreadSafe -public class FileSystemSource extends AbstractPathRepositorySource implements ObjectFactory { +public class FileSystemSource extends AbstractRepositorySource implements ObjectFactory { /** * An immutable {@link CustomPropertiesFactory} implementation that is used by default when none is provided. Note that this @@ -133,6 +136,8 @@ public class FileSystemSource extends AbstractPathRepositorySource implements Ob private transient FileSystemRepository repository; private volatile CustomPropertiesFactory customPropertiesFactory; + private ExecutionContext defaultContext = new ExecutionContext(); + /** * */ @@ -527,8 +532,22 @@ public class FileSystemSource extends AbstractPathRepositorySource implements Ob throw new RepositorySourceException(getName(), msg.text("name")); } - if (repository == null) repository = new FileSystemRepository(this); - return new PathRepositoryConnection(this, repository); + if (repository == null) { + repository = new FileSystemRepository(this); + + ExecutionContext context = repositoryContext != null ? repositoryContext.getExecutionContext() : defaultContext; + FileSystemTransaction txn = repository.startTransaction(context, false); + try { + // Create the set of initial workspaces ... + for (String initialName : getPredefinedWorkspaceNames()) { + repository.createWorkspace(txn, initialName, CreateConflictBehavior.DO_NOT_CREATE, null); + } + } finally { + txn.commit(); + } + + } + return new Connection(this, repository); } protected static class StandardPropertiesFactory implements CustomPropertiesFactory { Index: extensions/modeshape-connector-filesystem/src/main/java/org/modeshape/connector/filesystem/FileSystemWorkspace.java new file mode 100644 =================================================================== --- /dev/null (revision 1823) +++ extensions/modeshape-connector-filesystem/src/main/java/org/modeshape/connector/filesystem/FileSystemWorkspace.java (working copy) @@ -0,0 +1,552 @@ +package org.modeshape.connector.filesystem; + +import java.io.BufferedInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import org.modeshape.common.i18n.I18n; +import org.modeshape.common.util.FileUtil; +import org.modeshape.common.util.IoUtil; +import org.modeshape.graph.ExecutionContext; +import org.modeshape.graph.JcrLexicon; +import org.modeshape.graph.JcrNtLexicon; +import org.modeshape.graph.Location; +import org.modeshape.graph.ModeShapeLexicon; +import org.modeshape.graph.connector.RepositorySourceException; +import org.modeshape.graph.connector.base.PathNode; +import org.modeshape.graph.connector.base.PathWorkspace; +import org.modeshape.graph.mimetype.MimeTypeDetector; +import org.modeshape.graph.property.Binary; +import org.modeshape.graph.property.BinaryFactory; +import org.modeshape.graph.property.DateTimeFactory; +import org.modeshape.graph.property.Name; +import org.modeshape.graph.property.NameFactory; +import org.modeshape.graph.property.NamespaceRegistry; +import org.modeshape.graph.property.Path; +import org.modeshape.graph.property.PathFactory; +import org.modeshape.graph.property.PathNotFoundException; +import org.modeshape.graph.property.Property; +import org.modeshape.graph.property.PropertyFactory; +import org.modeshape.graph.property.Path.Segment; +import org.modeshape.graph.request.Request; + +/** + * Workspace implementation for the file system connector. + */ +class FileSystemWorkspace extends PathWorkspace { + private static final String DEFAULT_MIME_TYPE = "application/octet"; + private static final Set VALID_PRIMARY_TYPES = new HashSet(Arrays.asList(new Name[] {JcrNtLexicon.FOLDER, + JcrNtLexicon.FILE, JcrNtLexicon.RESOURCE, ModeShapeLexicon.RESOURCE})); + + private final FileSystemSource source; + private final FileSystemRepository repository; + private final ExecutionContext context; + private final File workspaceRoot; + + public FileSystemWorkspace( String name, + FileSystemWorkspace originalToClone, + File workspaceRoot ) { + super(name, originalToClone.getRootNodeUuid()); + + this.source = originalToClone.source; + this.context = originalToClone.context; + this.workspaceRoot = workspaceRoot; + this.repository = originalToClone.repository; + + cloneWorkspace(originalToClone); + } + + public FileSystemWorkspace( FileSystemRepository repository, + String name ) { + super(name, repository.getRootNodeUuid()); + this.workspaceRoot = repository.getWorkspaceDirectory(name); + this.repository = repository; + this.context = repository.getContext(); + this.source = repository.source; + } + + private void cloneWorkspace( FileSystemWorkspace original ) { + File originalRoot = repository.getWorkspaceDirectory(original.getName()); + File newRoot = repository.getWorkspaceDirectory(this.getName()); + + try { + FileUtil.copy(originalRoot, newRoot); + } catch (IOException ioe) { + throw new IllegalStateException(ioe); + } + } + + @Override + public PathNode moveNode( PathNode node, + PathNode newNode ) { + PathFactory pathFactory = context.getValueFactories().getPathFactory(); + Path newPath = pathFactory.create(newNode.getParent(), newNode.getName()); + + File originalFile = fileFor(pathFactory.create(node.getParent(), node.getName())); + File newFile = fileFor(newPath, false); + + if (newFile.exists()) { + newFile.delete(); + } + + originalFile.renameTo(newFile); + + return getNode(newPath); + } + + @Override + public PathNode putNode( PathNode node ) { + NameFactory nameFactory = context.getValueFactories().getNameFactory(); + PathFactory pathFactory = context.getValueFactories().getPathFactory(); + NamespaceRegistry registry = context.getNamespaceRegistry(); + CustomPropertiesFactory customPropertiesFactory = source.customPropertiesFactory(); + + Map properties = node.getProperties(); + + if (node.getParent() == null) { + // Root node + Path rootPath = pathFactory.createRootPath(); + Location rootLocation = Location.create(rootPath, repository.getRootNodeUuid()); + customPropertiesFactory.recordDirectoryProperties(context, + source.getName(), + rootLocation, + workspaceRoot, + node.getProperties()); + return getNode(rootPath); + } + + /* + * Get references to java.io.Files + */ + Path parentPath = node.getParent(); + boolean isRoot = parentPath == null; + File parentFile = fileFor(parentPath); + + Path newPath = isRoot ? pathFactory.createRootPath() : pathFactory.create(parentPath, node.getName()); + Name name = node.getName().getName(); + String newName = name.getString(registry); + File newFile = new File(parentFile, newName); + + /* + * Determine the node primary type + */ + Property primaryTypeProp = properties.get(JcrLexicon.PRIMARY_TYPE); + + // Default primary type to nt:folder + Name primaryType = primaryTypeProp == null ? JcrNtLexicon.FOLDER : nameFactory.create(primaryTypeProp.getFirstValue()); + + if (JcrNtLexicon.FILE.equals(primaryType)) { + + // The FILE node is represented by the existence of the file + if (!parentFile.canWrite()) { + I18n msg = FileSystemI18n.parentIsReadOnly; + throw new RepositorySourceException(source.getName(), msg.text(parentPath, this.getName(), source.getName())); + } + + try { + ensureValidPathLength(newFile); + + // Don't try to write if the node conflict behavior is DO_NOT_REPLACE + if (!newFile.createNewFile()) { + I18n msg = FileSystemI18n.fileAlreadyExists; + throw new RepositorySourceException(source.getName(), msg.text(parentPath, getName(), source.getName())); + } + } catch (IOException ioe) { + I18n msg = FileSystemI18n.couldNotCreateFile; + throw new RepositorySourceException(source.getName(), msg.text(parentPath, + getName(), + source.getName(), + ioe.getMessage()), ioe); + } + + customPropertiesFactory.recordFileProperties(context, source.getName(), Location.create(newPath), newFile, properties); + } else if (JcrNtLexicon.RESOURCE.equals(primaryType) || ModeShapeLexicon.RESOURCE.equals(primaryType)) { + assert parentFile != null; + + if (!JcrLexicon.CONTENT.equals(name)) { + I18n msg = FileSystemI18n.invalidNameForResource; + String nodeName = name.getString(); + throw new RepositorySourceException(source.getName(), msg.text(parentPath, getName(), source.getName(), nodeName)); + } + + if (!parentFile.isFile()) { + I18n msg = FileSystemI18n.invalidPathForResource; + throw new RepositorySourceException(source.getName(), msg.text(parentPath, getName(), source.getName())); + } + + if (!parentFile.canWrite()) { + I18n msg = FileSystemI18n.parentIsReadOnly; + throw new RepositorySourceException(source.getName(), msg.text(parentPath, getName(), source.getName())); + } + + // Copy over data into a temp file, then move it to the correct location + FileOutputStream fos = null; + try { + File temp = File.createTempFile("dna", null); + fos = new FileOutputStream(temp); + + Property dataProp = properties.get(JcrLexicon.DATA); + if (dataProp == null) { + I18n msg = FileSystemI18n.missingRequiredProperty; + String dataPropName = JcrLexicon.DATA.getString(); + throw new RepositorySourceException(source.getName(), msg.text(parentPath, + getName(), + source.getName(), + dataPropName)); + } + + BinaryFactory binaryFactory = context.getValueFactories().getBinaryFactory(); + Binary binary = binaryFactory.create(properties.get(JcrLexicon.DATA).getFirstValue()); + + IoUtil.write(binary.getStream(), fos); + + if (!FileUtil.delete(parentFile)) { + I18n msg = FileSystemI18n.deleteFailed; + throw new RepositorySourceException(source.getName(), msg.text(parentPath, getName(), source.getName())); + } + + if (!temp.renameTo(parentFile)) { + I18n msg = FileSystemI18n.couldNotUpdateData; + throw new RepositorySourceException(source.getName(), msg.text(parentPath, getName(), source.getName())); + } + } catch (IOException ioe) { + I18n msg = FileSystemI18n.couldNotWriteData; + throw new RepositorySourceException(source.getName(), msg.text(parentPath, + getName(), + source.getName(), + ioe.getMessage()), ioe); + + } finally { + try { + if (fos != null) fos.close(); + } catch (Exception ex) { + } + } + customPropertiesFactory.recordResourceProperties(context, + source.getName(), + Location.create(parentPath), + newFile, + properties); + + } else if (JcrNtLexicon.FOLDER.equals(primaryType) || primaryType == null) { + ensureValidPathLength(newFile); + + if (!newFile.exists() && !newFile.mkdir()) { + I18n msg = FileSystemI18n.couldNotCreateFile; + throw new RepositorySourceException(source.getName(), + msg.text(parentPath, + getName(), + source.getName(), + primaryType == null ? "null" : primaryType.getString(registry))); + } + customPropertiesFactory.recordDirectoryProperties(context, + source.getName(), + Location.create(newPath), + newFile, + properties); + + } else { + // Set error and return + I18n msg = FileSystemI18n.unsupportedPrimaryType; + throw new RepositorySourceException(source.getName(), msg.text(primaryType.getString(registry), + parentPath, + getName(), + source.getName())); + } + + node = getNode(newPath); + + return node; + } + + @Override + public PathNode removeNode( Path nodePath ) { + File nodeFile; + + if (!nodePath.isRoot() && JcrLexicon.CONTENT.equals(nodePath.getLastSegment().getName())) { + nodeFile = fileFor(nodePath.getParent()); + if (!nodeFile.exists()) return null; + + FileOutputStream fos = null; + try { + fos = new FileOutputStream(nodeFile); + IoUtil.write("", fos); + } catch (IOException ioe) { + throw new RepositorySourceException(source.getName(), FileSystemI18n.deleteFailed.text(nodePath, + getName(), + source.getName())); + } finally { + if (fos != null) try { + fos.close(); + } catch (IOException ioe) { + } + } + } else { + nodeFile = fileFor(nodePath); + if (!nodeFile.exists()) return null; + + FileUtil.delete(nodeFile); + } + + return null; + } + + @Override + public PathNode getRootNode() { + return getNode(context.getValueFactories().getPathFactory().createRootPath()); + } + + @Override + public PathNode getNode( Path path ) { + Map properties = new HashMap(); + + PropertyFactory factory = context.getPropertyFactory(); + PathFactory pathFactory = context.getValueFactories().getPathFactory(); + DateTimeFactory dateFactory = context.getValueFactories().getDateFactory(); + MimeTypeDetector mimeTypeDetector = context.getMimeTypeDetector(); + CustomPropertiesFactory customPropertiesFactory = source.customPropertiesFactory(); + NamespaceRegistry registry = context.getNamespaceRegistry(); + Location location = Location.create(path); + + if (!path.isRoot() && JcrLexicon.CONTENT.equals(path.getLastSegment().getName())) { + File file = fileFor(path.getParent()); + if (file == null) return null; + // Discover the mime type ... + String mimeType = null; + InputStream contents = null; + try { + contents = new BufferedInputStream(new FileInputStream(file)); + mimeType = mimeTypeDetector.mimeTypeOf(file.getName(), contents); + if (mimeType == null) mimeType = DEFAULT_MIME_TYPE; + properties.put(JcrLexicon.MIMETYPE, factory.create(JcrLexicon.MIMETYPE, mimeType)); + } catch (IOException e) { + I18n msg = FileSystemI18n.couldNotReadData; + throw new RepositorySourceException(source.getName(), msg.text(source.getName(), + getName(), + path.getString(registry))); + } finally { + if (contents != null) { + try { + contents.close(); + } catch (IOException e) { + } + } + } + + // First add any custom properties ... + Collection customProps = customPropertiesFactory.getResourceProperties(context, location, file, mimeType); + for (Property customProp : customProps) { + properties.put(customProp.getName(), customProp); + } + + // The request is to get properties of the "jcr:content" child node ... + // ... use the dna:resource node type. This is the same as nt:resource, but is not referenceable + // since we cannot assume that we control all access to this file and can track its movements + properties.put(JcrLexicon.PRIMARY_TYPE, factory.create(JcrLexicon.PRIMARY_TYPE, ModeShapeLexicon.RESOURCE)); + properties.put(JcrLexicon.LAST_MODIFIED, factory.create(JcrLexicon.LAST_MODIFIED, + dateFactory.create(file.lastModified()))); + // Don't really know the encoding, either ... + // request.addProperty(factory.create(JcrLexicon.ENCODED, stringFactory.create("UTF-8"))); + + // Now put the file's content into the "jcr:data" property ... + BinaryFactory binaryFactory = context.getValueFactories().getBinaryFactory(); + properties.put(JcrLexicon.DATA, factory.create(JcrLexicon.DATA, binaryFactory.create(file))); + // return new PathNode(path, null, properties, Collections.emptyList()); + return new PathNode(null, path.getParent(), path.getLastSegment(), properties, Collections.emptyList()); + } + + File file = fileFor(path); + if (file == null) return null; + + if (file.isDirectory()) { + String[] childNames = file.list(source.filenameFilter()); + Arrays.sort(childNames); + + List childSegments = new ArrayList(childNames.length); + for (String childName : childNames) { + childSegments.add(pathFactory.createSegment(childName)); + } + + Collection customProps = customPropertiesFactory.getDirectoryProperties(context, location, file); + for (Property customProp : customProps) { + properties.put(customProp.getName(), customProp); + } + + if (path.isRoot()) { + properties.put(JcrLexicon.PRIMARY_TYPE, factory.create(JcrLexicon.PRIMARY_TYPE, ModeShapeLexicon.ROOT)); + // return new DefaultPathNode(path, source.getRootNodeUuidObject(), properties, childSegments); + return new PathNode(source.getRootNodeUuidObject(), path.getParent(), path.getLastSegment(), properties, + childSegments); + + } + properties.put(JcrLexicon.PRIMARY_TYPE, factory.create(JcrLexicon.PRIMARY_TYPE, JcrNtLexicon.FOLDER)); + // return new DefaultPathNode(path, source.getRootNodeUuidObject(), properties, childSegments); + return new PathNode(null, path.getParent(), path.getLastSegment(), properties, childSegments); + + } + + Collection customProps = customPropertiesFactory.getFileProperties(context, location, file); + for (Property customProp : customProps) { + properties.put(customProp.getName(), customProp); + } + properties.put(JcrLexicon.PRIMARY_TYPE, factory.create(JcrLexicon.PRIMARY_TYPE, JcrNtLexicon.FILE)); + properties.put(JcrLexicon.CREATED, factory.create(JcrLexicon.CREATED, dateFactory.create(file.lastModified()))); + // node = new DefaultPathNode(path, null, properties, + // Collections.singletonList(pathFactory.createSegment(JcrLexicon.CONTENT))); + return new PathNode(null, path.getParent(), path.getLastSegment(), properties, + Collections.singletonList(pathFactory.createSegment(JcrLexicon.CONTENT))); + } + /** + * This utility files the existing {@link File} at the supplied path, and in the process will verify that the path is actually + * valid. + *

+ * Note that this connector represents a file as two nodes: a parent node with a name that matches the file and a " + * jcr:primaryType" of "nt:file"; and a child node with the name "jcr:content " and a " + * jcr:primaryType" of "nt:resource". The parent "nt:file" node and its properties + * represents the file itself, whereas the child "nt:resource" node and its properties represent the content of + * the file. + *

+ *

+ * As such, this method will return the File object for paths representing both the parent "nt:file " and child " + * nt:resource" node. + *

+ * + * @param path + * @return the existing {@link File file} for the path; or null if the path does not represent an existing file and a + * {@link PathNotFoundException} was set as the {@link Request#setError(Throwable) error} on the request + */ + protected File fileFor( Path path ) { + return fileFor(path, true); + } + + /** + * This utility files the existing {@link File} at the supplied path, and in the process will verify that the path is actually + * valid. + *

+ * Note that this connector represents a file as two nodes: a parent node with a name that matches the file and a " + * jcr:primaryType" of "nt:file"; and a child node with the name "jcr:content " and a " + * jcr:primaryType" of "nt:resource". The parent "nt:file" node and its properties + * represents the file itself, whereas the child "nt:resource" node and its properties represent the content of + * the file. + *

+ *

+ * As such, this method will return the File object for paths representing both the parent "nt:file " and child " + * nt:resource" node. + *

+ * + * @param path + * @param existingFilesOnly + * @return the existing {@link File file} for the path; or null if the path does not represent an existing file and a + * {@link PathNotFoundException} was set as the {@link Request#setError(Throwable) error} on the request + */ + protected File fileFor( Path path, + boolean existingFilesOnly ) { + if (path == null || path.isRoot()) { + return workspaceRoot; + } + // See if the path is a "jcr:content" node ... + if (path.getLastSegment().getName().equals(JcrLexicon.CONTENT)) { + // We only want to use the parent path to find the actual file ... + path = path.getParent(); + } + File file = workspaceRoot; + for (Path.Segment segment : path) { + String localName = segment.getName().getLocalName(); + // Verify the segment is valid ... + if (segment.getIndex() > 1) { + I18n msg = FileSystemI18n.sameNameSiblingsAreNotAllowed; + throw new RepositorySourceException(source.getName(), msg.text(source.getName())); + } + + String defaultNamespaceUri = context.getNamespaceRegistry().getDefaultNamespaceUri(); + if (!segment.getName().getNamespaceUri().equals(defaultNamespaceUri)) { + I18n msg = FileSystemI18n.onlyTheDefaultNamespaceIsAllowed; + throw new RepositorySourceException(source.getName(), msg.text(source.getName())); + } + + // The segment should exist as a child of the file ... + file = new File(file, localName); + + if (existingFilesOnly && (!file.canRead() || !file.exists())) { + return null; + } + } + assert file != null; + return file; + } + + protected void validate( PathNode node ) { + // Don't validate the root node + if (node.getParent() == null) return; + + NameFactory nameFactory = context.getValueFactories().getNameFactory(); + Map properties = node.getProperties(); + Property primaryTypeProp = properties.get(JcrLexicon.PRIMARY_TYPE); + Name primaryType = primaryTypeProp == null ? JcrNtLexicon.FOLDER : nameFactory.create(primaryTypeProp.getFirstValue()); + + if (!VALID_PRIMARY_TYPES.contains(primaryType)) { + // Set error and return + I18n msg = FileSystemI18n.unsupportedPrimaryType; + NamespaceRegistry registry = context.getNamespaceRegistry(); + Path parentPath = node.getParent(); + throw new RepositorySourceException(source.getName(), msg.text(primaryType.getString(registry), + parentPath, + getName(), + source.getName())); + + } + + Path nodePath = context.getValueFactories().getPathFactory().create(node.getParent(), node.getName()); + ensureValidPathLength(fileFor(nodePath, false)); + } + + protected void ensureValidPathLength( File file ) { + ensureValidPathLength(file, 0); + } + + /** + * Recursively checks if any of the files in the tree rooted at {@code root} would exceed the + * {@link FileSystemSource#getMaxPathLength() maximum path length for the processor} if their paths were {@code delta} + * characters longer. If any files would exceed this length, a {@link RepositorySourceException} is thrown. + * + * @param root the root of the tree to check; may be a file or directory but may not be null + * @param delta the change in the length of the path to check. Used to preemptively check whether moving a file or directory + * to a new path would violate path length rules + * @throws RepositorySourceException if any files in the tree rooted at {@code root} would exceed this + * {@link FileSystemSource#getMaxPathLength() the maximum path length for this processor} + */ + protected void ensureValidPathLength( File root, + int delta ) { + try { + int len = root.getCanonicalPath().length(); + if (len > source.getMaxPathLength() - delta) { + String msg = FileSystemI18n.maxPathLengthExceeded.text(source.getMaxPathLength(), + source.getName(), + root.getCanonicalPath(), + delta); + throw new RepositorySourceException(source.getName(), msg); + } + + if (root.isDirectory()) { + for (File child : root.listFiles(source.filenameFilter())) { + ensureValidPathLength(child, delta); + } + + } + } catch (IOException ioe) { + throw new RepositorySourceException(source.getName(), FileSystemI18n.getCanonicalPathFailed.text(), ioe); + } + } + +} Index: extensions/modeshape-connector-filesystem/src/main/java/org/modeshape/connector/filesystem/package-info.java =================================================================== --- extensions/modeshape-connector-filesystem/src/main/java/org/modeshape/connector/filesystem/package-info.java (revision 1823) +++ extensions/modeshape-connector-filesystem/src/main/java/org/modeshape/connector/filesystem/package-info.java (working copy) @@ -23,7 +23,9 @@ */ /** * The classes that make up the connector that accesses the files and directories on a local file system and exposes them as content in a repository. - * This connector is based on the {@link org.modeshape.graph.connector.path.WritablePathRepository path repository framework}. + * This connector is based on the {@link org.modeshape.graph.connector.base.PathWorkspace path repository framework}. + * @see org.modeshape.graph.connector.base.PathWorkspace + * @see org.modeshape.graph.connector.base.PathTransaction */ package org.modeshape.connector.filesystem; Index: extensions/modeshape-connector-filesystem/src/main/resources/org/modeshape/connector/filesystem/FileSystemI18n.properties =================================================================== --- extensions/modeshape-connector-filesystem/src/main/resources/org/modeshape/connector/filesystem/FileSystemI18n.properties (revision 1823) +++ extensions/modeshape-connector-filesystem/src/main/resources/org/modeshape/connector/filesystem/FileSystemI18n.properties (working copy) @@ -27,8 +27,7 @@ pathForWorkspaceRootDoesNotExist = The path "{0}" for the predefined workspace f pathForWorkspaceRootIsNotDirectory = The path "{0}" for the predefined workspace for the file system source "{1}" is actually a path to an existing file pathForWorkspaceRootCannotBeRead = The path "{0}" for the predefined workspace for the file system source "{1}" cannot be read propertyIsRequired = The {0} property is required but has no value -locationInRequestMustHavePath = {0} requires a path in the request: {1} -sameNameSiblingsAreNotAllowed = Repository source "{0}" does not allow same name siblings on nodes: {1} +sameNameSiblingsAreNotAllowed = The "{0}" source does not allow same-name siblings nodeOrderingNotSupported = {0} does not support node ordering onlyTheDefaultNamespaceIsAllowed = Repository source "{0}" requires that node names use the default namespace sourceIsReadOnly = The source "{0}" does not allow updates. Set the "updatesAllowed" property to "true" on the repository source (connector) to enable updates. @@ -52,6 +51,5 @@ couldNotWriteData = Error writing data to path "{0}" in workspace "{1}" in {2}\: couldNotUpdateData = Error moving temporary data file to path "{0}" in workspace "{1}" in {2} missingRequiredProperty = Missing required property "{3}" at path "{0}" in workspace "{1}" in {2} deleteFailed = Could not delete file at path "{0}" in workspace "{1}" in {2} -copyFailed = Could not copy file at path "{0}" in workspace "{1}" to path "{2}" in workspace "{3}" in {4} getCanonicalPathFailed = Could not determine canonical path maxPathLengthExceeded = The maximum absolute path length ({0}) for source "{1}" was exceeded by the node at: {2} ({3}) Index: extensions/modeshape-connector-filesystem/src/test/java/org/modeshape/connector/filesystem/FileSystemConnectorCreateWorkspacesTest.java =================================================================== --- extensions/modeshape-connector-filesystem/src/test/java/org/modeshape/connector/filesystem/FileSystemConnectorCreateWorkspacesTest.java (revision 1823) +++ extensions/modeshape-connector-filesystem/src/test/java/org/modeshape/connector/filesystem/FileSystemConnectorCreateWorkspacesTest.java (working copy) @@ -27,11 +27,11 @@ import static org.hamcrest.core.Is.is; import static org.junit.Assert.assertThat; import java.util.HashSet; import java.util.Set; +import org.junit.Test; import org.modeshape.graph.Graph; import org.modeshape.graph.Workspace; import org.modeshape.graph.connector.RepositorySource; import org.modeshape.graph.connector.test.WorkspaceConnectorTest; -import org.junit.Test; /** * These tests verify that the file system connector behaves correctly when the source is configured to @@ -56,6 +56,7 @@ public class FileSystemConnectorCreateWorkspacesTest extends WorkspaceConnectorT source.setPredefinedWorkspaceNames(predefinedWorkspaceNames); source.setDefaultWorkspaceName(predefinedWorkspaceNames[0]); source.setCreatingWorkspacesAllowed(true); + source.setUpdatesAllowed(true); return source; } Index: extensions/modeshape-connector-filesystem/src/test/java/org/modeshape/connector/filesystem/FileSystemConnectorWritableTest.java =================================================================== --- extensions/modeshape-connector-filesystem/src/test/java/org/modeshape/connector/filesystem/FileSystemConnectorWritableTest.java (revision 1823) +++ extensions/modeshape-connector-filesystem/src/test/java/org/modeshape/connector/filesystem/FileSystemConnectorWritableTest.java (working copy) @@ -24,6 +24,7 @@ package org.modeshape.connector.filesystem; import static org.hamcrest.core.Is.is; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; @@ -32,11 +33,12 @@ import java.io.FileInputStream; import java.io.IOException; import org.junit.Test; import org.modeshape.common.util.FileUtil; +import org.modeshape.common.util.StringUtil; import org.modeshape.graph.Graph; import org.modeshape.graph.JcrLexicon; -import org.modeshape.graph.JcrMixLexicon; import org.modeshape.graph.JcrNtLexicon; import org.modeshape.graph.ModeShapeLexicon; +import org.modeshape.graph.Graph.Batch; import org.modeshape.graph.connector.RepositorySource; import org.modeshape.graph.connector.RepositorySourceException; import org.modeshape.graph.connector.test.AbstractConnectorTest; @@ -185,11 +187,6 @@ public class FileSystemConnectorWritableTest extends AbstractConnectorTest { graph.create("/testFile").with(JcrLexicon.PRIMARY_TYPE, JcrNtLexicon.UNSTRUCTURED).orReplace().and(); } - @Test( expected = RepositorySourceException.class ) - public void shouldNotBeAbleToSetArbitraryProperties() { - graph.create("/testFile").with(JcrLexicon.MIXIN_TYPES, JcrMixLexicon.LOCKABLE).orReplace().and(); - } - @Test public void shouldBeAbleToCopyFile() { graph.create("/testFile").with(JcrLexicon.PRIMARY_TYPE, JcrNtLexicon.FILE).orReplace().and(); @@ -515,6 +512,32 @@ public class FileSystemConnectorWritableTest extends AbstractConnectorTest { } } + @Test + public void shouldBeAllOrNothing() { + String longTestFileName = "/testFileWithTooLongName" + StringUtil.createString('x', 300); + + Batch batch = graph.batch(); + + batch.create("/testFile").with(JcrLexicon.PRIMARY_TYPE, JcrNtLexicon.FILE).orReplace().and(); + batch.create("/testFile/jcr:content") + .with(JcrLexicon.PRIMARY_TYPE, ModeShapeLexicon.RESOURCE) + .and(JcrLexicon.DATA, TEST_CONTENT.getBytes()) + .orReplace() + .and(); + batch.create(longTestFileName).and(); + + try { + batch.execute(); + fail("The overly long test file name (" + longTestFileName + ") did not fail"); + } catch (RepositorySourceException rse) { + // Expected + } + + File newFile = new File(testWorkspaceRoot, "testFile"); + assertFalse(newFile.exists()); + } + + protected void assertContents( File file, String contents ) { assertTrue(file.exists()); Index: extensions/modeshape-connector-filesystem/testFile new file mode 100644 =================================================================== --- /dev/null (revision 1823) +++ extensions/modeshape-connector-filesystem/testFile (working copy) @@ -0,0 +1 @@ +Test content \ No newline at end of file Index: extensions/modeshape-connector-filesystem/testFolder/testFile new file mode 100644 =================================================================== --- /dev/null (revision 1823) +++ extensions/modeshape-connector-filesystem/testFolder/testFile (working copy) @@ -0,0 +1 @@ +Test content \ No newline at end of file Index: modeshape-graph/src/main/java/org/modeshape/graph/connector/base/AbstractRepositorySource.java new file mode 100644 =================================================================== --- /dev/null (revision 1823) +++ modeshape-graph/src/main/java/org/modeshape/graph/connector/base/AbstractRepositorySource.java (working copy) @@ -0,0 +1,197 @@ +package org.modeshape.graph.connector.base; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; +import javax.naming.BinaryRefAddr; +import javax.naming.RefAddr; +import javax.naming.Reference; +import javax.naming.StringRefAddr; +import org.modeshape.common.util.CheckArg; +import org.modeshape.graph.cache.CachePolicy; +import org.modeshape.graph.connector.RepositoryContext; +import org.modeshape.graph.connector.RepositorySourceException; + +/** + * Basic implementation of {@link BaseRepositorySource}, providing default implementations of the accessors and mutators in that + * interface. + */ +@SuppressWarnings( "serial" ) +public abstract class AbstractRepositorySource implements BaseRepositorySource { + + /** + * The default UUID that is used for root nodes in a store. + */ + public static final String DEFAULT_ROOT_NODE_UUID = "cafebabe-cafe-babe-cafe-babecafebabe"; + + /** + * The default number of times that a request that failed due to system error should be retried + */ + public static final int DEFAULT_RETRY_LIMIT = 0; + + /** + * The default cache policy for this repository source (no caching) + */ + public static final CachePolicy DEFAULT_CACHE_POLICY = null; + + protected int retryLimit = DEFAULT_RETRY_LIMIT; + protected String name; + + protected transient RepositoryContext repositoryContext; + protected transient UUID rootNodeUuid = UUID.fromString(DEFAULT_ROOT_NODE_UUID); + protected transient CachePolicy cachePolicy = DEFAULT_CACHE_POLICY; + + /** + * {@inheritDoc} + * + * @see org.modeshape.graph.connector.base.BaseRepositorySource#areUpdatesAllowed() + */ + public boolean areUpdatesAllowed() { + return false; + } + + /** + * {@inheritDoc} + * + * @see org.modeshape.graph.connector.base.BaseRepositorySource#getRepositoryContext() + */ + public RepositoryContext getRepositoryContext() { + return repositoryContext; + } + + /** + * {@inheritDoc} + * + * @see org.modeshape.graph.connector.RepositorySource#initialize(RepositoryContext) + */ + public void initialize( RepositoryContext context ) throws RepositorySourceException { + CheckArg.isNotNull(context, "context"); + this.repositoryContext = context; + } + + /** + * {@inheritDoc} + * + * @see org.modeshape.graph.connector.base.BaseRepositorySource#getDefaultCachePolicy() + */ + public CachePolicy getDefaultCachePolicy() { + return this.cachePolicy; + } + + /** + * Sets the cache policy for the repository and replaces the path repository cache with a new path repository cache tied to + * the new cache policy + * + * @param cachePolicy the new cache policy; may not be null + */ + public void setCachePolicy( CachePolicy cachePolicy ) { + CheckArg.isNotNull(cachePolicy, "cachePolicy"); + this.cachePolicy = cachePolicy; + } + + /** + * {@inheritDoc} + * + * @see org.modeshape.graph.connector.base.BaseRepositorySource#getRootNodeUuidObject() + */ + public UUID getRootNodeUuidObject() { + return rootNodeUuid; + } + + /** + * @param rootNodeUuid Sets rootNodeUuid to the specified value. + * @throws IllegalArgumentException if the string value cannot be converted to UUID + */ + public void setRootNodeUuidObject( String rootNodeUuid ) { + if (rootNodeUuid != null && rootNodeUuid.trim().length() == 0) rootNodeUuid = DEFAULT_ROOT_NODE_UUID; + this.rootNodeUuid = UUID.fromString(rootNodeUuid); + } + + /** + * {@inheritDoc} + * + * @see org.modeshape.graph.connector.RepositorySource#close() + */ + public void close() { + } + + /** + * {@inheritDoc} + * + * @see org.modeshape.graph.connector.RepositorySource#getName() + */ + public String getName() { + return name; + } + + /** + * Sets the name of the repository source. The name should be unique among loaded repository sources. + * + * @param name the new name for the repository source; may not be empty + */ + public void setName( String name ) { + if (name != null) { + name = name.trim(); + if (name.length() == 0) name = null; + } + + this.name = name; + } + + /** + * {@inheritDoc} + * + * @see org.modeshape.graph.connector.RepositorySource#getRetryLimit() + */ + public int getRetryLimit() { + return retryLimit; + } + + /** + * {@inheritDoc} + * + * @see org.modeshape.graph.connector.RepositorySource#setRetryLimit(int limit) + */ + public void setRetryLimit( int limit ) { + this.retryLimit = limit < 0 ? 0 : limit; + } + + /** + * Extracts the values from the given reference, automatically translating {@link BinaryRefAddr} instances into the + * deserialized classes that they represent. + * + * @param ref the reference from which the values should be extracted + * @return a map of value names to values from the reference + * @throws IOException if there is an error deserializing a {@code BinaryRefAddr} + * @throws ClassNotFoundException if a serialized class cannot be deserialized because its class is not in the class path + */ + protected Map valuesFrom( Reference ref ) throws IOException, ClassNotFoundException { + Map values = new HashMap(); + + Enumeration en = ref.getAll(); + while (en.hasMoreElements()) { + RefAddr subref = (RefAddr)en.nextElement(); + if (subref instanceof StringRefAddr) { + String key = subref.getType(); + Object value = subref.getContent(); + if (value != null) values.put(key, value.toString()); + } else if (subref instanceof BinaryRefAddr) { + String key = subref.getType(); + Object value = subref.getContent(); + if (value instanceof byte[]) { + // Deserialize ... + ByteArrayInputStream bais = new ByteArrayInputStream((byte[])value); + ObjectInputStream ois = new ObjectInputStream(bais); + value = ois.readObject(); + values.put(key, value); + } + } + } + + return values; + } +} Index: modeshape-graph/src/main/java/org/modeshape/graph/connector/base/BaseTransaction.java =================================================================== --- modeshape-graph/src/main/java/org/modeshape/graph/connector/base/BaseTransaction.java (revision 1823) +++ modeshape-graph/src/main/java/org/modeshape/graph/connector/base/BaseTransaction.java (working copy) @@ -25,6 +25,7 @@ package org.modeshape.graph.connector.base; import java.util.LinkedList; import java.util.List; +import java.util.Set; import java.util.UUID; import net.jcip.annotations.NotThreadSafe; import org.modeshape.graph.ExecutionContext; @@ -55,7 +56,11 @@ public abstract class BaseTransaction repository; + protected BaseTransaction( ExecutionContext context, + Repository repository, UUID rootNodeUuid ) { this.rootNodeUuid = rootNodeUuid; this.context = context; @@ -63,6 +68,7 @@ public abstract class BaseTransaction getRepository() { + return repository; + } + + /** + * {@inheritDoc} + * + * @see org.modeshape.graph.connector.base.Transaction#getWorkspaceNames() + */ + public Set getWorkspaceNames() { + return repository.getWorkspaceNames(); + } + + /** * {@inheritDoc} * * @see org.modeshape.graph.connector.base.Transaction#getRootNode(org.modeshape.graph.connector.base.Workspace) Index: modeshape-graph/src/main/java/org/modeshape/graph/connector/base/MapTransaction.java =================================================================== --- modeshape-graph/src/main/java/org/modeshape/graph/connector/base/MapTransaction.java (revision 1823) +++ modeshape-graph/src/main/java/org/modeshape/graph/connector/base/MapTransaction.java (working copy) @@ -64,8 +64,6 @@ import org.modeshape.graph.request.FullTextSearchRequest; public abstract class MapTransaction> extends BaseTransaction { - /** The repository against which this transaction is operating */ - private final Repository repository; /** The set of changes to the workspaces that have been made by this transaction */ private Map changesByWorkspaceName; @@ -77,17 +75,7 @@ public abstract class MapTransaction repository, UUID rootNodeUuid ) { - super(repository.getContext(), rootNodeUuid); - this.repository = repository; - } - - /** - * Obtain the repository object against which this transaction is running. - * - * @return the repository object; never null - */ - protected Repository getRepository() { - return repository; + super(repository.getContext(), repository, rootNodeUuid); } /** @@ -690,7 +678,7 @@ public abstract class MapTransaction + * Strictly speaking, this class is not immutable or thread safe. However, the persisted state cannot be changed. Instead, any + * changes made to the object are stored in a transient area and are made "persistable"via the {@link #freeze()} method. + *

+ *

+ * The {@link PathTransaction} maintains an unfrozen, changed instance within it transactional state, and always puts the + * {@link #freeze() frozen}, read-only representation inside the + */ +public class PathNode implements Node, Serializable, Cloneable { + + private static final long serialVersionUID = 1L; + + /* These members MUST be treated as "final", even though they cannot be to correctly implement Serializable */ + private/*final*/UUID uuid; + private/*final*/Path parent; + private/*final*/Segment name; + private/*final*/Map properties; + private/*final*/List children; + private/*final*/int version = 1; + + /** The changes made to this object, making it unfrozen */ + protected transient Changes changes; + + /** + * Create a new node instance. + * + * @param uuid the UUID of the node; may be null + * @param parent the path to the parent node; may be null only if the name is null + * @param name the name of this node, relative to the parent + * @param properties the unmodifiable map of properties; may be null or empty + * @param children the unmodifiable list of child segments; may be null or empty + */ + public PathNode( UUID uuid, + Path parent, + Segment name, + Map properties, + List children ) { + this.uuid = uuid; + this.parent = parent; + this.name = name; + this.properties = properties != null ? properties : Collections.emptyMap(); + this.children = children != null ? children : Collections.emptyList(); + assert this.properties != null; + assert this.children != null; + assert this.name != null ? this.parent != null : this.parent == null; + } + + /** + * Create a new node instance. + * + * @param uuid the UUID of the node; may be null + * @param parent the path to the parent node; may be null only if the name is null + * @param name the name of this node, relative to the parent + * @param properties the unmodifiable map of properties; may be null or empty + * @param children the unmodifiable list of child segments; may be null or empty + * @param version the version number + */ + protected PathNode( UUID uuid, + Path parent, + Segment name, + Map properties, + List children, + int version ) { + this.uuid = uuid; + this.parent = parent; + this.name = name; + this.properties = properties != null ? properties : Collections.emptyMap(); + this.children = children != null ? children : Collections.emptyList(); + this.version = version; + assert this.properties != null; + assert this.children != null; + assert this.name != null ? this.parent != null : this.parent == null; + } + + /** + * Create a new node instance. + * + * @param uuid the UUID of the node; may be null + * @param parent the path to the parent node; may be null only if the name is null + * @param name the name of this node, relative to the parent + * @param properties the properties that are to be copied into the new node; may be null or empty + * @param children the unmodifiable list of child segments; may be null or empty + */ + public PathNode( UUID uuid, + Path parent, + Segment name, + Iterable properties, + List children ) { + this.uuid = uuid; + this.parent = parent; + this.name = name; + if (properties != null) { + Map props = new HashMap(); + for (Property prop : properties) { + props.put(prop.getName(), prop); + } + this.properties = props.isEmpty() ? Collections.emptyMap() : Collections.unmodifiableMap(props); + } else { + this.properties = Collections.emptyMap(); + } + this.children = children != null ? children : Collections.emptyList(); + assert this.properties != null; + assert this.children != null; + assert this.name != null ? this.parent != null : this.parent == null; + } + + /** + * Create a root node with the supplied UUID. + * + * @param uuid the UUID of the root node; may not be null + */ + public PathNode( UUID uuid ) { + this.uuid = uuid; + this.parent = null; + this.name = null; + this.properties = Collections.emptyMap(); + this.children = Collections.emptyList(); + assert this.uuid != null; + assert this.properties != null; + assert this.children != null; + } + + /** + * Get the version number of this node. + * + * @return the version number + */ + public int getVersion() { + return version; + } + + /** + * {@inheritDoc} + * + * @see org.modeshape.graph.connector.base.Node#getUuid() + */ + public UUID getUuid() { + return uuid; + } + + /** + * {@inheritDoc} + * + * @see org.modeshape.graph.connector.base.Node#getName() + */ + public Segment getName() { + return changes != null ? changes.getName() : name; + } + + /** + * @return parent + */ + public Path getParent() { + return changes != null ? changes.getParent() : parent; + } + + /** + * {@inheritDoc} + * + * @see org.modeshape.graph.connector.base.Node#getProperties() + */ + public Map getProperties() { + return changes != null ? changes.getProperties(false) : properties; + } + + /** + * {@inheritDoc} + * + * @see org.modeshape.graph.connector.base.Node#getProperty(org.modeshape.graph.property.Name) + */ + public Property getProperty( Name name ) { + return getProperties().get(name); + } + + /** + * @return children + */ + public List getChildren() { + return changes != null ? changes.getChildren(false) : children; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((children == null) ? 0 : children.hashCode()); + result = prime * result + ((name == null) ? 0 : name.hashCode()); + result = prime * result + ((parent == null) ? 0 : parent.hashCode()); + return result; + } + + @Override + public boolean equals( Object obj ) { + if (this == obj) return true; + if (obj == null) return false; + if (getClass() != obj.getClass()) return false; + PathNode other = (PathNode)obj; + if (name == null) { + if (other.name != null) return false; + } else if (!name.equals(other.name)) return false; + if (parent == null) { + if (other.parent != null) return false; + } else if (!parent.equals(other.parent)) return false; + return true; + } + + /** + * {@inheritDoc} + * + * @see java.lang.Object#toString() + */ + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + if (this.parent == null) { + sb.append("/"); + } else { + sb.append(this.getParent()).append("/").append(this.getName()); + } + sb.append(" ("); + sb.append(this.getUuid()).append(")"); + return sb.toString(); + } + + /** + * {@inheritDoc} + *

+ * This method never clones the {@link #hasChanges() changes}. + *

+ * + * @see java.lang.Object#clone() + */ + @Override + public PathNode clone() { + return new PathNode(uuid, parent, name, new HashMap(properties), new ArrayList(children)); + } + + /** + * Determine if this node has any unsaved changes. + * + * @return true if there are unsaved changes, or false otherwise + */ + protected boolean hasChanges() { + return changes != null; + } + + /** + * Create the {@link Changes} implementation. Subclasses that require a specialized class should overwrite this method. Note + * that this method does not modify any internal state; it should just instantiate and return the correct Changes class. + * + * @return the changes object. + */ + protected Changes newChanges() { + return new Changes(); + } + + /** + * Return the frozen node with all internal state reflective of any changes. If this node has no changes, this method simply + * returns this same node. Otherwise, this method creates a new node that has no changes and that mirrors this node's current + * state, and this new node will have an incremented {@link #getVersion() version} number. + * + * @return the unfrozen node; never null + */ + public PathNode freeze() { + if (!hasChanges()) return this; + return new PathNode(uuid, changes.getParent(), changes.getName(), changes.getUnmodifiableProperties(), + changes.getUnmodifiableChildren(), version + 1); + } + + /** + * Create a copy of this node except using the supplied path. + * + * @param parent sets parent to the specified value. + * @return the new path node; never null + */ + public PathNode withParent( Path parent ) { + if (changes == null) { + PathNode copy = clone(); + copy.changes = newChanges(); + copy.changes.setParent(parent); + return copy; + } + changes.setParent(parent); + return this; + } + + /** + * Create a copy of this node except using the supplied name. + * + * @param name sets name to the specified value. + * @return the new path node; never null + */ + public PathNode withName( Segment name ) { + if (changes == null) { + PathNode copy = clone(); + copy.changes = newChanges(); + copy.changes.setName(name); + return copy; + } + changes.setName(name); + return this; + } + + /** + * Create a copy of this node except adding the supplied node at the end of the existing children. + * + * @param child the segment of the child that is to be added; may not be null + * @return the new path node; never null + */ + public PathNode withChild( Segment child ) { + assert child != null; + if (getChildren().indexOf(child) != -1) return this; + if (changes == null) { + PathNode copy = clone(); + List children = new LinkedList(getChildren()); + assert !children.contains(child); + children.add(child); + copy.changes = newChanges(); + copy.changes.setChildren(children); + return copy; + } + changes.getChildren(true).add(child); + return this; + } + + /** + * Create a copy of this node except adding the supplied node into the existing children at the specified index. + * + * @param index the index at which the child is to appear + * @param child the segment of the child that is to be added at the end of the existing children + * @return the new path node; never null + */ + public PathNode withChild( int index, + Segment child ) { + assert child != null; + assert index >= 0; + int existingIndex = getChildren().indexOf(child); + if (existingIndex == index) { + // No need to add twice, so simply return (have not yet made any changes) + return this; + } + if (changes == null) { + PathNode copy = clone(); + List children = new LinkedList(getChildren()); + if (existingIndex >= 0) { + // The child is moving positions, so remove it before we add it ... + children.remove(existingIndex); + if (existingIndex < index) --index; + } + children.add(index, child); + copy.changes = newChanges(); + copy.changes.setChildren(children); + return copy; + } + List children = changes.getChildren(true); + if (existingIndex >= 0) { + // The child is moving positions, so remove it before we add it ... + children.remove(existingIndex); + if (existingIndex < index) --index; + } + children.add(index, child); + return this; + } + + /** + * Create a copy of this node except without the supplied child node. + * + * @param child the segment of the child that is to be removed; may not be null + * @return the new path node; never null + */ + public PathNode withoutChild( Segment child ) { + assert child != null; + if (changes == null) { + PathNode copy = clone(); + List children = new LinkedList(getChildren()); + children.remove(child); + copy.changes = newChanges(); + copy.changes.setChildren(children); + return copy; + } + changes.getChildren(true).remove(child); + return this; + } + + /** + * Create a copy of this node except with none of the children. + * + * @return the new path node; never null + */ + public PathNode withoutChildren() { + if (getChildren().isEmpty()) return this; + if (changes == null) { + PathNode copy = clone(); + copy.changes = newChanges(); + copy.changes.setChildren(new LinkedList()); + return copy; + } + changes.getChildren(true).clear(); + return this; + } + + /** + * Create a copy of this node except with the changes to the properties. + * + * @param propertiesToSet the properties that are to be set; may be null if no properties are to be set + * @param propertiesToRemove the names of the properties that are to be removed; may be null if no properties are to be + * removed + * @param removeAllExisting true if all existing properties should be removed + * @return the unfrozen path node; never null + */ + public PathNode withProperties( Iterable propertiesToSet, + Iterable propertiesToRemove, + boolean removeAllExisting ) { + if (propertiesToSet == null && propertiesToRemove == null && !removeAllExisting) { + // no changes ... + return this; + } + Map newProperties = null; + PathNode result = this; + if (changes == null) { + PathNode copy = clone(); + copy.changes = newChanges(); + copy.changes.setProperties(new HashMap(this.properties)); + newProperties = copy.changes.getProperties(true); + result = copy; + } else { + newProperties = changes.getProperties(true); + } + if (removeAllExisting) { + newProperties.clear(); + } else { + if (propertiesToRemove != null) { + for (Name name : propertiesToRemove) { + newProperties.remove(name); + } + } else if (propertiesToSet == null) { + return this; + } + } + if (propertiesToSet != null) { + for (Property property : propertiesToSet) { + newProperties.put(property.getName(), property); + } + } + return result; + } + + /** + * Create a copy of this node except with the new property. + * + * @param property the property to set + * @return this path node + */ + public PathNode withProperty( Property property ) { + if (property == null) return this; + if (changes == null) { + PathNode copy = clone(); + copy.changes = newChanges(); + Map newProps = new HashMap(this.properties); + newProps.put(property.getName(), property); + copy.changes.setProperties(newProps); + return copy; + } + changes.getProperties(true).put(property.getName(), property); + return this; + } + + /** + * Create a copy of this node except without the new property. + * + * @param propertyName the name of the property that is to be removed + * @return this path node, or this node if the named properties does not exist on this node + */ + public PathNode withoutProperty( Name propertyName ) { + if (propertyName == null || !getProperties().containsKey(propertyName)) return this; + if (changes == null) { + PathNode copy = clone(); + copy.changes = newChanges(); + copy.changes.setProperties(new HashMap(this.properties)); + return copy; + } + changes.getProperties(true).remove(propertyName); + return this; + } + + /** + * Create a copy of this node without any properties + * + * @return this path node, or this node if this node has no properties + */ + public PathNode withoutProperties() { + if (getProperties().isEmpty()) return this; + if (changes == null) { + PathNode copy = clone(); + copy.changes = newChanges(); + copy.changes.setProperties(new HashMap()); + return copy; + } + changes.getProperties(true).clear(); + return this; + + } + + @SuppressWarnings( "synthetic-access" ) + protected class Changes { + private Path parent; + private Segment name; + private Map properties; + private List children; + + public Path getParent() { + return parent != null ? parent : PathNode.this.parent; + } + + public void setParent( Path parent ) { + this.parent = parent; + } + + public Segment getName() { + return name != null ? name : PathNode.this.name; + } + + public void setName( Segment name ) { + this.name = name; + } + + public Map getProperties( boolean createIfMissing ) { + if (properties == null) { + if (createIfMissing) { + properties = new HashMap(PathNode.this.properties); + return properties; + } + return PathNode.this.properties; + } + return properties; + } + + public Map getUnmodifiableProperties() { + return properties != null ? Collections.unmodifiableMap(properties) : PathNode.this.properties; + } + + public void setProperties( Map properties ) { + this.properties = properties; + } + + public List getChildren( boolean createIfMissing ) { + if (children == null) { + if (createIfMissing) { + children = new LinkedList(); + return children; + } + return PathNode.this.children; + } + return children; + } + + public List getUnmodifiableChildren() { + return children != null ? Collections.unmodifiableList(children) : PathNode.this.children; + } + + public void setChildren( List children ) { + this.children = children; + } + } + +} Index: modeshape-graph/src/main/java/org/modeshape/graph/connector/base/PathTransaction.java new file mode 100644 =================================================================== --- /dev/null (revision 1823) +++ modeshape-graph/src/main/java/org/modeshape/graph/connector/base/PathTransaction.java (working copy) @@ -0,0 +1,808 @@ +/* + * ModeShape (http://www.modeshape.org) + * See the COPYRIGHT.txt file distributed with this work for information + * regarding copyright ownership. Some portions may be licensed + * to Red Hat, Inc. under one or more contributor license agreements. + * See the AUTHORS.txt file in the distribution for a full listing of + * individual contributors. + * + * ModeShape is free software. Unless otherwise indicated, all code in ModeShape + * is licensed to you under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * ModeShape is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this software; if not, write to the Free + * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA, or see the FSF site: http://www.fsf.org. + */ +package org.modeshape.graph.connector.base; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.ListIterator; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; +import java.util.UUID; +import net.jcip.annotations.NotThreadSafe; +import org.modeshape.common.util.StringUtil; +import org.modeshape.graph.GraphI18n; +import org.modeshape.graph.Location; +import org.modeshape.graph.connector.RepositorySourceException; +import org.modeshape.graph.connector.base.PathWorkspace.ChangeCommand; +import org.modeshape.graph.property.Name; +import org.modeshape.graph.property.Path; +import org.modeshape.graph.property.PathNotFoundException; +import org.modeshape.graph.property.Property; +import org.modeshape.graph.property.Path.Segment; +import org.modeshape.graph.query.QueryResults; +import org.modeshape.graph.request.AccessQueryRequest; +import org.modeshape.graph.request.FullTextSearchRequest; + +/** + * An implementation of {@link Transaction} that maintains a cache of nodes by their path. + * + * @param the type of workspace + * @param the type of node + */ +@NotThreadSafe +public abstract class PathTransaction> + extends BaseTransaction { + + /** The set of changes to the workspaces that have been made by this transaction */ + private Map changesByWorkspaceName; + + /** + * Create a new transaction. + * + * @param repository the repository against which the transaction will be operating; may not be null + * @param rootNodeUuid the UUID of the root node; may not be null + */ + protected PathTransaction( Repository repository, + UUID rootNodeUuid ) { + super(repository.getContext(), repository, rootNodeUuid); + } + + /** + * Get the changes for the supplied workspace, optionally creating the necessary object if it does not yet exist. The changes + * object is used to record the changes made to the workspace by operations within this transaction, which are either pushed + * into the workspace upon {@link #commit()} or cleared upon {@link #rollback()}. + * + * @param workspace the workspace + * @param createIfMissing true if the changes object should be created if it does not yet exist, or false otherwise + * @return the changes object; may be null if createIfMissing is false and the changes object does + * not yet exist, or never null if createIfMissing is true + */ + protected WorkspaceChanges getChangesFor( WorkspaceType workspace, + boolean createIfMissing ) { + if (changesByWorkspaceName == null) { + if (!createIfMissing) return null; + WorkspaceChanges changes = new WorkspaceChanges(workspace); + changesByWorkspaceName = new HashMap(); + changesByWorkspaceName.put(workspace.getName(), changes); + return changes; + } + WorkspaceChanges changes = changesByWorkspaceName.get(workspace.getName()); + if (changes == null && createIfMissing) { + changes = new WorkspaceChanges(workspace); + changesByWorkspaceName.put(workspace.getName(), changes); + } + return changes; + } + + /** + * {@inheritDoc} + * + * @see org.modeshape.graph.connector.base.Transaction#getNode(org.modeshape.graph.connector.base.Workspace, + * org.modeshape.graph.Location) + */ + public NodeType getNode( WorkspaceType workspace, + Location location ) { + assert location != null; + // First look for the UUID ... + UUID uuid = location.getUuid(); + if (getRepository().getRootNodeUuid().equals(uuid)) { + Path rootPath = pathFactory.createRootPath(); + + // The root node can't be removed + WorkspaceChanges changes = getChangesFor(workspace, false); + NodeType node = null; + if (changes != null) { + assert !changes.isRemoved(rootPath); + // Not deleted, but maybe changed in this transaction ... + node = changes.getChangedOrAdded(rootPath); + if (node != null) return node; + } + // It hasn't been loaded already, so attempt to load it from the map owned by the workspace ... + node = workspace.getNode(rootPath); + if (node != null) return node; + } + // Otherwise, look by path ... + if (location.hasPath()) { + return getNode(workspace, location.getPath(), location); + } + // Unable to find by UUID or by path, so fail ... + Path lowestExisting = pathFactory.createRootPath(); + throw new PathNotFoundException(location, lowestExisting, GraphI18n.nodeDoesNotExist.text(readable(location))); + } + + /** + * Attempt to find the node with the supplied path. This method is "Changes-aware". That is, it checks the cache of changes + * for the workspace before returning the node. + * + * @param workspace the workspace; may not be null + * @param path the path of the node; may not be null + * @return the node, or null if no such node exists + */ + protected NodeType findNode( WorkspaceType workspace, + Path path ) { + WorkspaceChanges changes = getChangesFor(workspace, false); + NodeType node = null; + if (changes != null) { + // See if the node we're looking for was deleted ... + if (changes.isRemoved(path)) { + // This node was removed within this transaction ... + return null; + } + // Not deleted, but maybe changed in this transaction ... + node = changes.getChangedOrAdded(path); + if (node != null) return node; + } + // It hasn't been loaded already, so attempt to load it from the map owned by the workspace ... + node = workspace.getNode(path); + return node; + } + + /** + * Destroy the node. + * + * @param workspace the workspace; never null + * @param node the node to be destroyed + */ + protected void destroyNode( WorkspaceType workspace, + NodeType node ) { + WorkspaceChanges changes = getChangesFor(workspace, true); + destroyNode(changes, workspace, node); + } + + /** + * Destroy the node and it's contents. + * + * @param changes the record of the workspace changes; never null + * @param workspace the workspace; never null + * @param node the node to be destroyed + */ + private void destroyNode( WorkspaceChanges changes, + WorkspaceType workspace, + NodeType node ) { + + changes.removed(pathTo(node)); + } + + /** + * Returns the location for the given node, based on its path + * + * @param node the node for which the location should be returned; may not be null + * @return the location for the given node, based on its path; never null + */ + private Location locationFor( NodeType node ) { + return Location.create(pathTo(node)); + } + + /** + * Returns the path to the given node + * + * @param node the node for which the path should be returned; may not be null + * @return the path to that node + */ + protected Path pathTo( NodeType node ) { + if (node.getParent() == null) { + return pathFactory.createRootPath(); + } + return pathFactory.create(node.getParent(), node.getName()); + } + + /** + * {@inheritDoc} + * + * @see org.modeshape.graph.connector.base.Transaction#addChild(org.modeshape.graph.connector.base.Workspace, + * org.modeshape.graph.connector.base.Node, org.modeshape.graph.property.Name, int, java.util.UUID, java.lang.Iterable) + */ + @SuppressWarnings( "unchecked" ) + public NodeType addChild( WorkspaceType workspace, + NodeType parent, + Name name, + int index, + UUID uuid, + Iterable properties ) { + assert uuid == null : "UUID should always be null for a PathTransaction"; + WorkspaceChanges changes = getChangesFor(workspace, true); + + // If the parent doesn't already have changes, we need to find the new parent in the newWorkspace's changes + if (!parent.hasChanges()) { + parent = getNode(workspace, locationFor(parent)); + } + + NodeType newNode = null; + if (index < 0) { + // Figure out the SNS of the new node ... + int snsIndex = 1; + for (NodeType child : getChildren(workspace, parent)) { + if (child.getName().getName().equals(name)) ++snsIndex; + } + + // Create the new node ... + Segment childSegment = pathFactory.createSegment(name, snsIndex); + newNode = createNode(childSegment, pathTo(parent), properties); + // And add to the parent ... + parent = (NodeType)parent.withChild(childSegment); + } else { + int snsIndex = 0; + List children = getChildren(workspace, parent); + + if (index < children.size()) { + ListIterator existingSiblings = children.listIterator(index); + while (existingSiblings.hasNext()) { + NodeType existingSibling = existingSiblings.next(); + Segment existingSegment = existingSibling.getName(); + if (existingSegment.getName().equals(name)) { + int existingIndex = existingSegment.getIndex(); + if (snsIndex == 0) snsIndex = existingIndex; + existingSibling = (NodeType)existingSibling.withName(pathFactory.createSegment(name, existingIndex + 1)); + changes.changed(existingSibling); + } + } + } + // Create the new node ... + Segment childSegment = pathFactory.createSegment(name, snsIndex + 1); + newNode = createNode(childSegment, pathTo(parent), properties); + // And add to the parent ... + parent = (NodeType)parent.withChild(index, childSegment); + } + changes.created(newNode); + changes.changed(parent); + return newNode; + } + + /** + * {@inheritDoc} + * + * @see org.modeshape.graph.connector.base.Transaction#addChild(org.modeshape.graph.connector.base.Workspace, + * org.modeshape.graph.connector.base.Node, org.modeshape.graph.connector.base.Node, + * org.modeshape.graph.connector.base.Node, org.modeshape.graph.property.Name) + */ + @SuppressWarnings( "unchecked" ) + public Location addChild( WorkspaceType workspace, + NodeType parent, + NodeType newChild, + NodeType beforeOtherChild, + Name desiredName ) { + // If the parent doesn't already have changes, we need to find the new parent in the newWorkspace's changes + if (!parent.hasChanges()) { + parent = findNode(workspace, pathTo(parent)); + } + + // Get some information about the child ... + Segment newChildSegment = newChild.getName(); + Name newChildName = newChildSegment.getName(); + int snsIndex = newChildSegment.getIndex(); + + // Find the existing parent of the new child ... + NodeType oldParent = getParent(workspace, newChild); + + // Find the changes for this workspace ... + WorkspaceChanges changes = getChangesFor(workspace, true); + NodeType newChildWithOldParent = null; + + if (oldParent != null) { + newChildWithOldParent = newChild; + // Remove the node from it's parent ... + int oldIndex = oldParent.getChildren().indexOf(newChildSegment); + if (oldParent.equals(parent)) { + oldParent = (NodeType)oldParent.withoutChild(newChildSegment); + changes.changed(oldParent); + parent = oldParent; + } else { + oldParent = (NodeType)oldParent.withoutChild(newChildSegment); + changes.changed(oldParent); + } + + // Now find any siblings with the same name that appear after the node in the parent's list of children ... + List siblings = getChildren(workspace, oldParent); + if (oldIndex < siblings.size()) { + for (ListIterator iter = siblings.listIterator(oldIndex); iter.hasNext();) { + NodeType sibling = iter.next(); + if (sibling.getName().getName().equals(newChildName)) { + sibling = (NodeType)sibling.withName(pathFactory.createSegment(newChildName, snsIndex++)); + changes.changed(sibling); + } + } + } + } + + // Find the index of the other child ... + int index = parent.getChildren().size(); + if (beforeOtherChild != null) { + if (!beforeOtherChild.getParent().equals(pathTo(parent))) { + // The other child doesn't exist in the parent ... + throw new RepositorySourceException(null); + } + Segment otherChild = beforeOtherChild.getName(); + index = parent.getChildren().indexOf(otherChild); + } + + // Determine the desired new name for the node ... + newChildName = desiredName != null ? desiredName : newChildName; + + // Find the SNS index for the new child ... + ListIterator existingSiblings = getChildren(workspace, parent).listIterator(); // makes a copy + int i = 0; + snsIndex = 1; + Segment childName = null; + while (existingSiblings.hasNext()) { + NodeType existingSibling = existingSiblings.next(); + Segment existingSegment = existingSibling.getName(); + if (i < index) { + // Nodes before the insertion point + if (existingSegment.getName().equals(newChildName)) { + ++snsIndex; + } + } else { + if (i == index) { + // Add the child node ... + childName = pathFactory.createSegment(newChildName, snsIndex); + } + if (existingSegment.getName().equals(newChildName)) { + existingSibling = (NodeType)existingSibling.withName(pathFactory.createSegment(newChildName, ++snsIndex)); + changes.changed(existingSibling); + } + } + ++i; + } + if (childName == null) { + // Must be appending the child ... + childName = pathFactory.createSegment(newChildName, snsIndex); + } + + // Change the name of the new node ... + newChild = (NodeType)newChild.withName(childName).withParent(pathTo(parent)); + parent = (NodeType)parent.withChild(index, newChild.getName()); + changes.moved(newChildWithOldParent, newChild, parent); + + return locationFor(newChild); + } + + /** + * Create a new instance of the node, given the supplied name and parent path. This method should do nothing but instantiate + * the new node; the caller will add to the appropriate internal structures. + * + * @param name the name of the new node; may be null if this is the root node + * @param parentPath the path of the parent node; may be null if this is the root node + * @param properties the properties; may be null if there are no properties + * @return the new node; never null + */ + protected abstract NodeType createNode( Segment name, + Path parentPath, + Iterable properties ); + + /** + * {@inheritDoc} + * + * @see org.modeshape.graph.connector.base.Transaction#getChild(org.modeshape.graph.connector.base.Workspace, + * org.modeshape.graph.connector.base.Node, org.modeshape.graph.property.Path.Segment) + */ + @SuppressWarnings( "unchecked" ) + public NodeType getChild( WorkspaceType workspace, + NodeType parent, + Segment childSegment ) { + List children = parent.getChildren(); // don't make a copy + for (Segment child : children) { + if (child.equals(childSegment)) { + Path childPath = pathFactory.create(pathTo(parent), child); + + WorkspaceChanges changes = getChangesFor(workspace, true); + NodeType changed = changes.getChangedOrAdded(childPath); + if (changed != null) { + return changed; + } + + Path persistentPath = changes.persistentPathFor(childPath); + NodeType childNode = workspace.getNode(persistentPath); + + if (persistentPath.equals(childPath)) return childNode; + + return (NodeType)childNode.withParent(childPath.getParent()).withName(childPath.getLastSegment()); + } + } + return null; + } + + /** + * {@inheritDoc} + * + * @see org.modeshape.graph.connector.base.Transaction#getChildren(org.modeshape.graph.connector.base.Workspace, + * org.modeshape.graph.connector.base.Node) + */ + public List getChildren( WorkspaceType workspace, + NodeType node ) { + List childSegments = node.getChildren(); // make a copy + if (childSegments.isEmpty()) return Collections.emptyList(); + + List children = new ArrayList(childSegments.size()); + + for (Segment childSegment : childSegments) { + children.add(getNode(workspace, Location.create(pathFactory.create(pathTo(node), childSegment)))); + } + + return children; + } + + /** + * {@inheritDoc} + * + * @see org.modeshape.graph.connector.base.Transaction#getParent(org.modeshape.graph.connector.base.Workspace, + * org.modeshape.graph.connector.base.Node) + */ + public NodeType getParent( WorkspaceType workspace, + NodeType node ) { + Path parentPath = node.getParent(); + if (parentPath == null) return null; + return getNode(workspace, Location.create(parentPath)); + } + + /** + * {@inheritDoc} + * + * @see org.modeshape.graph.connector.base.Transaction#removeAllChildren(org.modeshape.graph.connector.base.Workspace, + * org.modeshape.graph.connector.base.Node) + */ + @SuppressWarnings( "unchecked" ) + public void removeAllChildren( WorkspaceType workspace, + NodeType node ) { + for (NodeType child : getChildren(workspace, node)) { + destroyNode(workspace, child); + } + node = (NodeType)node.withoutChildren(); + getChangesFor(workspace, true).changed(node); + } + + /** + * {@inheritDoc} + * + * @see org.modeshape.graph.connector.base.Transaction#removeNode(org.modeshape.graph.connector.base.Workspace, + * org.modeshape.graph.connector.base.Node) + */ + @SuppressWarnings( "unchecked" ) + public Location removeNode( WorkspaceType workspace, + NodeType node ) { + NodeType parent = getParent(workspace, node); + if (parent == null) { + // The root node is being removed, which means we should just delete everything (except the root) ... + WorkspaceChanges changes = getChangesFor(workspace, true); + changes.removeAll(null); + return Location.create(pathFactory.createRootPath(), rootNodeUuid); + } + Location result = locationFor(node); + + // Find the index of the node in it's parent ... + int index = parent.getChildren().indexOf(node.getName()); + assert index != -1; + Name name = node.getName().getName(); + int snsIndex = node.getName().getIndex(); + WorkspaceChanges changes = getChangesFor(workspace, true); + // Remove the node from the parent ... + parent = (NodeType)parent.withoutChild(node.getName()); + changes.changed(parent); + + // Now find any siblings with the same name that appear after the node in the parent's list of children ... + List siblings = getChildren(workspace, parent); + if (index < siblings.size()) { + for (ListIterator iter = siblings.listIterator(index); iter.hasNext();) { + NodeType sibling = iter.next(); + if (sibling.getName().getName().equals(name)) { + sibling = (NodeType)sibling.withName(pathFactory.createSegment(name, snsIndex++)); + changes.changed(sibling); + } + } + } + // Destroy the subgraph starting at the node, and record the change on the parent ... + destroyNode(changes, workspace, node); + return result; + } + + /** + * {@inheritDoc} + * + * @see org.modeshape.graph.connector.base.Transaction#removeProperty(org.modeshape.graph.connector.base.Workspace, + * org.modeshape.graph.connector.base.Node, org.modeshape.graph.property.Name) + */ + @SuppressWarnings( "unchecked" ) + public NodeType removeProperty( WorkspaceType workspace, + NodeType node, + Name propertyName ) { + NodeType copy = (NodeType)node.withoutProperty(propertyName); + if (copy != node) { + WorkspaceChanges changes = getChangesFor(workspace, true); + changes.changed(copy); + } + return copy; + } + + /** + * {@inheritDoc} + * + * @see org.modeshape.graph.connector.base.Transaction#setProperties(org.modeshape.graph.connector.base.Workspace, + * org.modeshape.graph.connector.base.Node, java.lang.Iterable, java.lang.Iterable, boolean) + */ + @SuppressWarnings( "unchecked" ) + public NodeType setProperties( WorkspaceType workspace, + NodeType node, + Iterable propertiesToSet, + Iterable propertiesToRemove, + boolean removeAllExisting ) { + NodeType copy = (NodeType)node.withProperties(propertiesToSet, propertiesToRemove, removeAllExisting); + if (copy != node) { + WorkspaceChanges changes = getChangesFor(workspace, true); + changes.changed(copy); + } + return copy; + } + + /** + * {@inheritDoc} + * + * @see org.modeshape.graph.connector.base.Transaction#copyNode(org.modeshape.graph.connector.base.Workspace, + * org.modeshape.graph.connector.base.Node, org.modeshape.graph.connector.base.Workspace, + * org.modeshape.graph.connector.base.Node, org.modeshape.graph.property.Name, boolean) + */ + @SuppressWarnings( "unchecked" ) + public NodeType copyNode( WorkspaceType originalWorkspace, + NodeType original, + WorkspaceType newWorkspace, + NodeType newParent, + Name desiredName, + boolean recursive ) { + if (desiredName == null) desiredName = original.getName().getName(); + // Create a copy of the original under the new parent ... + NodeType copy = addChild(newWorkspace, newParent, desiredName, -1, null, original.getProperties().values()); + + if (recursive) { + WorkspaceChanges changes = getChangesFor(newWorkspace, true); + // Walk through the original branch in its workspace ... + for (NodeType originalChild : getChildren(originalWorkspace, original)) { + NodeType newChild = copyBranch(originalWorkspace, originalChild, changes, newWorkspace, copy); + copy = (NodeType)copy.withChild(newChild.getName()); + } + } + + // Record the latest changes on the newly-created node .. + WorkspaceChanges changes = getChangesFor(newWorkspace, true); + changes.changed(copy); + + return copy; + } + + protected void print( WorkspaceType workspace, + NodeType node, + int level ) { + StringBuilder sb = new StringBuilder(); + sb.append(StringUtil.createString(' ', level * 2)); + sb.append(readable(node.getName())).append(" (").append(node.getUuid()).append(") {"); + boolean first = true; + for (Property property : node.getProperties().values()) { + if (first) first = false; + else sb.append(','); + sb.append(readable(property.getName())).append('='); + if (property.isMultiple()) sb.append(property.getValuesAsArray()); + else sb.append(readable(property.getFirstValue())); + } + sb.append('}'); + System.out.println(sb); + for (NodeType child : getChildren(workspace, node)) { + print(workspace, child, level + 1); + } + } + + /** + * {@inheritDoc} + * + * @see org.modeshape.graph.connector.base.Transaction#cloneNode(org.modeshape.graph.connector.base.Workspace, + * org.modeshape.graph.connector.base.Node, org.modeshape.graph.connector.base.Workspace, + * org.modeshape.graph.connector.base.Node, org.modeshape.graph.property.Name, org.modeshape.graph.property.Path.Segment, + * boolean, java.util.Set) + */ + public NodeType cloneNode( WorkspaceType originalWorkspace, + NodeType original, + WorkspaceType newWorkspace, + NodeType newParent, + Name desiredName, + Segment desiredSegment, + boolean removeExisting, + java.util.Set removedExistingNodes ) + throws org.modeshape.graph.connector.UuidAlreadyExistsException { + + return copyNode(originalWorkspace, original, newWorkspace, newParent, desiredName, true); + } + + @SuppressWarnings( "unchecked" ) + protected NodeType copyBranch( WorkspaceType originalWorkspace, + NodeType original, + WorkspaceChanges newWorkspaceChanges, + WorkspaceType newWorkspace, + NodeType newParent ) { + // Create the new node (or reuse the original if we can) ... + NodeType copy = createNode(original.getName(), pathTo(newParent), original.getProperties().values()); + newWorkspaceChanges.created(copy); + + // Walk through the children and call this method recursively ... + for (NodeType originalChild : getChildren(originalWorkspace, original)) { + NodeType newChild = copyBranch(originalWorkspace, originalChild, newWorkspaceChanges, newWorkspace, copy); + copy = (NodeType)copy.withChild(newChild.getName()); + } + newWorkspaceChanges.changed(copy); + return copy; + } + + /** + * {@inheritDoc} + *

+ * This implementation does not support querying the repository contents, so this method returns null. Subclasses can override + * this if they do support querying. + *

+ * + * @see org.modeshape.graph.connector.base.Transaction#query(org.modeshape.graph.connector.base.Workspace, + * org.modeshape.graph.request.AccessQueryRequest) + */ + public QueryResults query( WorkspaceType workspace, + AccessQueryRequest accessQuery ) { + return null; + } + + /** + * {@inheritDoc} + *

+ * This implementation does not support searching the repository contents, so this method returns null. Subclasses can + * override this if they do support searching. + *

+ * + * @see org.modeshape.graph.connector.base.Transaction#search(org.modeshape.graph.connector.base.Workspace, + * org.modeshape.graph.request.FullTextSearchRequest) + */ + public QueryResults search( WorkspaceType workspace, + FullTextSearchRequest search ) { + return null; + } + + protected void validateNode( WorkspaceType workspace, + NodeType node ) { + + } + + /** + * {@inheritDoc} + * + * @see org.modeshape.graph.connector.base.BaseTransaction#commit() + */ + @Override + public void commit() { + super.commit(); + // Push all of the changes or added nodes onto the workspace ... + if (changesByWorkspaceName != null) { + for (WorkspaceChanges changes : changesByWorkspaceName.values()) { + changes.commit(); + } + changesByWorkspaceName.clear(); + } + } + + /** + * {@inheritDoc} + * + * @see org.modeshape.graph.connector.base.BaseTransaction#rollback() + */ + @Override + public void rollback() { + super.rollback(); + if (changesByWorkspaceName != null) { + changesByWorkspaceName.clear(); + } + } + + /** + * Record of the changes made to a particular workspace. + */ + protected class WorkspaceChanges { + private final WorkspaceType workspace; + private final Map changedOrAddedNodes = new TreeMap(); + private final TreeMap movedNodes = new TreeMap(); + private final Set removedNodes = new HashSet(); + private final List> commands = new LinkedList>(); + + protected WorkspaceChanges( WorkspaceType workspace ) { + this.workspace = workspace; + } + + public WorkspaceType getWorkspace() { + return workspace; + } + + public void removeAll( NodeType newRootNode ) { + changedOrAddedNodes.clear(); + removedNodes.clear(); + commands.add(workspace.createRemoveCommand(pathFactory.createRootPath())); + } + + public boolean isRemoved( Path path ) { + return removedNodes.contains(path); + } + + public Path persistentPathFor( Path path ) { + for (Path newPath : movedNodes.descendingKeySet()) { + if (path.isAtOrBelow(newPath)) { + return path.relativeTo(newPath).resolveAgainst(movedNodes.get(newPath)); + } + } + return path; + } + + public NodeType getChangedOrAdded( Path path ) { + return changedOrAddedNodes.get(path); + } + + public void removed( Path path ) { + removedNodes.add(path); + changedOrAddedNodes.remove(path); + commands.add(workspace.createRemoveCommand(path)); + } + + public void created( NodeType node ) { + validateNode(workspace, node); + + Path path = pathTo(node); + removedNodes.remove(path); + changedOrAddedNodes.put(path, node); + commands.add(workspace.createPutCommand(node)); + } + + public void changed( NodeType node ) { + validateNode(workspace, node); + + Path path = pathTo(node); + assert !removedNodes.contains(path); // should not be removed + changedOrAddedNodes.put(path, node); + commands.add(workspace.createPutCommand(node)); + } + + public void moved( NodeType node, + NodeType newNode, + NodeType newParent ) { + validateNode(workspace, newNode); + + Path path = pathTo(node); + Path newPath = pathTo(newNode); + changedOrAddedNodes.put(newPath, newNode); + changedOrAddedNodes.put(pathTo(newParent), newParent); + movedNodes.put(pathTo(newNode), path); + commands.add(workspace.createMoveCommand(node, newNode)); + } + + public void commit() { + workspace.commit(commands); + } + + @Override + public String toString() { + return changedOrAddedNodes.keySet().toString() + "\n\t" + movedNodes.toString(); + } + } +} Index: modeshape-graph/src/main/java/org/modeshape/graph/connector/base/PathWorkspace.java new file mode 100644 =================================================================== --- /dev/null (revision 1823) +++ modeshape-graph/src/main/java/org/modeshape/graph/connector/base/PathWorkspace.java (working copy) @@ -0,0 +1,305 @@ +/* + * ModeShape (http://www.modeshape.org) + * See the COPYRIGHT.txt file distributed with this work for information + * regarding copyright ownership. Some portions may be licensed + * to Red Hat, Inc. under one or more contributor license agreements. + * See the AUTHORS.txt file in the distribution for a full listing of + * individual contributors. + * + * ModeShape is free software. Unless otherwise indicated, all code in ModeShape + * is licensed to you under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * ModeShape is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this software; if not, write to the Free + * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA, or see the FSF site: http://www.fsf.org. + */ +package org.modeshape.graph.connector.base; + +import java.util.List; +import java.util.UUID; +import java.util.concurrent.locks.ReadWriteLock; +import net.jcip.annotations.NotThreadSafe; +import org.modeshape.graph.property.Path; + +/** + * The {@link Workspace} implementation that represents all nodes as {@link PathNode} objects and stores them in an internal data + * structure that allows for nodes to be accessed via a {@link Path}. + *

+ * Subclasses are required to provide thread-safe access and modification of the state within the encapsulated data structure, + * since multiple {@link Transaction} implementations may be {@link Transaction#commit() committing} changes to the data structure + * at the same time. However, this class does not provide any thread-safety, since the nature of the thread-safety will almost + * certainly depend on the actual implementation. For example, a subclass may use a {@link ReadWriteLock lock}, or it may use a + * map implementation that provides the thread-safety. + *

+ * + * @param the type of node + */ +@NotThreadSafe +public abstract class PathWorkspace implements Workspace { + + private final String name; + private final UUID rootNodeUuid; + + /** + * Create a new instance of the workspace. + * + * @param name the workspace name; may not be null + * @param rootNodeUuid the root node that is expected to already exist in the map + */ + public PathWorkspace( String name, + UUID rootNodeUuid ) { + this.name = name; + this.rootNodeUuid = rootNodeUuid; + assert this.name != null; + assert this.rootNodeUuid != null; + } + + /** + * Create a new instance of the workspace. + * + * @param name the workspace name; may not be null + * @param originalToClone the workspace that is to be cloned; may not be null + */ + public PathWorkspace( String name, + PathWorkspace originalToClone ) { + this.name = name; + this.rootNodeUuid = originalToClone.getRootNode().getUuid(); + assert this.name != null; + assert this.rootNodeUuid != null; + throw new UnsupportedOperationException("Need to implement the ability to clone a workspace"); + } + + /** + * {@inheritDoc} + * + * @see org.modeshape.graph.connector.base.Workspace#getName() + */ + public String getName() { + return name; + } + + protected UUID getRootNodeUuid() { + return rootNodeUuid; + } + + /** + * Get the root node in this workspace. + * + * @return the root node; never null + */ + public abstract NodeType getRootNode(); + + /** + * Get the node with the supplied path. + * + * @param path the path to the node + * @return the node state as known by this workspace, or null if no such node exists in this workspace + */ + public abstract NodeType getNode( Path path ); + + /** + * Save this node into the workspace, overwriting any previous record of the node. This method should be overridden by + * writable path workspace implementations that use the default {@link ChangeCommand} implementations. + * + * @param node the new node; may not be null + * @return the previous node state, or null if the node is new to this workspace + * @throws UnsupportedOperationException by default, subclasses should override this method so that this exception is not + * thrown + * @see #createMoveCommand(PathNode, PathNode) + * @see #createPutCommand(PathNode) + * @see #createRemoveCommand(Path) + */ + public NodeType putNode( NodeType node ) { + throw new UnsupportedOperationException(); + } + + /** + * Move the node from it's previous location to the new location, overwriting any previous node at that location. This method + * should be overridden by writable path workspace implementations that use the default {@link ChangeCommand} implementations. + *

+ * The move operation is intended to reflect changes to the node's {@link PathNode#getName() name} or + * {@link PathNode#getParent() parent} only. Changes to the children or properties of the node should be reflected separately + * in a {@link #putNode(PathNode) put command} of some sort. The details of the put command are implementation-specific. + *

+ * + * @param source the original version of the node to be moved; may not be null + * @param target the new version (implying a change to the name or parent) of the node to be moved; may not be null + * @return the new node state;may not be null + * @throws UnsupportedOperationException by default, subclasses should override this method so that this exception is not + * thrown + * @see #createMoveCommand(PathNode, PathNode) + * @see #createPutCommand(PathNode) + * @see #createRemoveCommand(Path) + */ + public NodeType moveNode( NodeType source, + NodeType target ) { + throw new UnsupportedOperationException(); + } + + /** + * Remove this node and its descendants from the workspace. This method should be overridden by writable path workspace + * implementations that use the default {@link ChangeCommand} implementations. + * + * @param path the path to the node to be removed; may not be null + * @return the previous node state, or null if the node does not exist in this workspace + * @throws UnsupportedOperationException by default, subclasses should override this method so that this exception is not + * thrown + * @see #createMoveCommand(PathNode, PathNode) + * @see #createPutCommand(PathNode) + * @see #createRemoveCommand(Path) + */ + public NodeType removeNode( Path path ) { + throw new UnsupportedOperationException(); + } + + /** + * Remove all of the nodes in this workspace, and make sure there is a single root node with no properties and no children. + */ + public void removeAll() { + throw new UnsupportedOperationException(); + + } + + /** + * {@inheritDoc} + * + * @see java.lang.Object#toString() + */ + @Override + public String toString() { + return name; + } + + /** + * Successively (and in order) apply the changes from the list of pending commands + *

+ * All validation for each of the objects (including validation of resource availability in the underlying persistent store) + * should be performed prior to invoking this method. + *

+ * + * @param commands the list of commands to apply + */ + public void commit( List> commands ) { + for (ChangeCommand command : commands) { + command.apply(); + } + } + + /** + * Create a change command for the required update to the given node + * + * @param node the new version of the node; may not be null + * @return a {@link ChangeCommand} instance that reflects the changes to the node + * @see #createPutCommand(PathNode) + * @see #commit(List) + */ + public ChangeCommand createPutCommand( NodeType node ) { + return new PutCommand(node); + } + + /** + * Create a change command for the removal of the given node and its descendants + * + * @param path the path to the node at the root of the branch to be removed; may not be null + * @return a {@link ChangeCommand} instance that reflects the changes to the node + * @see #createPutCommand(PathNode) + * @see #commit(List) + */ + public ChangeCommand createRemoveCommand( Path path ) { + return new RemoveCommand(path); + } + + /** + * Create a change command that represents the movement of a node. The movement record will only reflect the changes to the + * node's name and/or parent. Changes to the node's properties or children should be ignored. A separate + * {@link #createPutCommand(PathNode) put command} should be used to reflect these changes. + * + * @param source the original version of the node; may not be null + * @param target the new version of the node; may not be null + * @return a {@link ChangeCommand} instance that reflects the changes to the node + * @see #createMoveCommand(PathNode, PathNode) + * @see #commit(List) + */ + public ChangeCommand createMoveCommand( NodeType source, + NodeType target ) { + return new MoveCommand(source, target); + } + + /** + * A specific operation that mutates the underlying persistent repository. + * + * @param the type of node against which this change should apply + */ + public interface ChangeCommand { + /** + * Make the change represented by this command permanent. + */ + void apply(); + } + + private class PutCommand implements ChangeCommand { + private NodeType node; + + protected PutCommand( NodeType node ) { + super(); + this.node = node; + } + + public void apply() { + PathWorkspace.this.putNode(node); + } + + @Override + public String toString() { + return "Put: { " + node + "}"; + } + } + + private class RemoveCommand implements ChangeCommand { + private Path path; + + protected RemoveCommand( Path path ) { + super(); + this.path = path; + } + + public void apply() { + PathWorkspace.this.removeNode(path); + } + + @Override + public String toString() { + return "Remove: { " + path.getString() + "}"; + } + } + + private class MoveCommand implements ChangeCommand { + private NodeType node; + private NodeType newNode; + + protected MoveCommand( NodeType node, + NodeType newNode ) { + super(); + this.node = node; + this.newNode = newNode; + } + + public void apply() { + PathWorkspace.this.moveNode(node, newNode); + } + + @Override + public String toString() { + return "Move: { " + node + " to " + newNode + "}"; + } + } + +} Index: modeshape-graph/src/main/java/org/modeshape/graph/connector/base/Processor.java =================================================================== --- modeshape-graph/src/main/java/org/modeshape/graph/connector/base/Processor.java (revision 1823) +++ modeshape-graph/src/main/java/org/modeshape/graph/connector/base/Processor.java (working copy) @@ -114,7 +114,15 @@ public class Processor e for (Node child : children) { Segment childName = child.getName(); Path childPath = pathFactory.create(path, childName); - request.addChild(childPath, propertyFactory.create(ModeShapeLexicon.UUID, child.getUuid())); + Location childLocation = null; + + if (child.getUuid() != null) { + childLocation = Location.create(childPath, child.getUuid()); + } else { + childLocation = Location.create(childPath); + } + + request.addChild(childLocation); } // Get the properties of the node ... @@ -147,7 +155,14 @@ public class Processor e for (Node child : children) { Segment childName = child.getName(); Path childPath = pathFactory.create(path, childName); - request.addChild(childPath, propertyFactory.create(ModeShapeLexicon.UUID, child.getUuid())); + Location childLocation = null; + + if (child.getUuid() != null) { + childLocation = Location.create(childPath, child.getUuid()); + } else { + childLocation = Location.create(childPath); + } + request.addChild(childLocation); } request.setActualLocationOfNode(actualLocation); setCacheableInfo(request); @@ -302,8 +317,13 @@ public class Processor e // See if the node already exists (this doesn't record an error on the request) ... node = txn.getFirstChild(workspace, parentNode, request.named()); if (node != null) { - txn.removeNode(workspace, node); - node = txn.addChild(workspace, parentNode, request.named(), 1, uuid, propsToStore); + List children = txn.getChildren(workspace, node); + for (NodeType child : children) { + txn.removeNode(workspace, child); + } + txn.setProperties(workspace, node, propsToStore, null, true); + // txn.removeNode(workspace, node); + // node = txn.addChild(workspace, parentNode, request.named(), 1, uuid, propsToStore); } else { node = txn.addChild(workspace, parentNode, request.named(), -1, uuid, propsToStore); } Index: modeshape-graph/src/main/java/org/modeshape/graph/connector/base/Transaction.java =================================================================== --- modeshape-graph/src/main/java/org/modeshape/graph/connector/base/Transaction.java (revision 1823) +++ modeshape-graph/src/main/java/org/modeshape/graph/connector/base/Transaction.java (working copy) @@ -45,7 +45,7 @@ import org.modeshape.graph.request.LockBranchRequest.LockScope; * A transaction in which all read and write operations against a repository are performed. The actual transaction instance is * obtained by calling {@link Repository#startTransaction(ExecutionContext,boolean)}. *

- * Note that implementations are not required to be thread-safe, since they (and their corresonding {@link Connection}) are + * Note that implementations are not required to be thread-safe, since they (and their corresponding {@link Connection}) are * expected to be used by a single thread. *

* Index: modeshape-graph/src/main/java/org/modeshape/graph/connector/inmemory/InMemoryTransaction.java =================================================================== --- modeshape-graph/src/main/java/org/modeshape/graph/connector/inmemory/InMemoryTransaction.java (revision 1823) +++ modeshape-graph/src/main/java/org/modeshape/graph/connector/inmemory/InMemoryTransaction.java (working copy) @@ -23,7 +23,6 @@ */ package org.modeshape.graph.connector.inmemory; -import java.util.Set; import java.util.UUID; import java.util.concurrent.locks.Lock; import net.jcip.annotations.NotThreadSafe; @@ -52,15 +51,6 @@ public class InMemoryTransaction extends MapTransaction getWorkspaceNames() { - return repository.getWorkspaceNames(); - } - - /** - * {@inheritDoc} - * * @see org.modeshape.graph.connector.base.Transaction#getWorkspace(java.lang.String, * org.modeshape.graph.connector.base.Workspace) */ Index: modeshape-graph/src/test/java/org/modeshape/graph/connector/base/MockPathNode.java new file mode 100644 =================================================================== --- /dev/null (revision 1823) +++ modeshape-graph/src/test/java/org/modeshape/graph/connector/base/MockPathNode.java (working copy) @@ -0,0 +1,58 @@ +package org.modeshape.graph.connector.base; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import org.modeshape.graph.property.Name; +import org.modeshape.graph.property.Path; +import org.modeshape.graph.property.Property; +import org.modeshape.graph.property.Path.Segment; + +@SuppressWarnings( "serial" ) +public class MockPathNode extends PathNode { + + public MockPathNode( UUID uuid, + Path parent, + Segment name, + Iterable properties, + List children ) { + super(uuid, parent, name, properties, children); + } + + public MockPathNode( UUID uuid, + Path parent, + Segment name, + Map properties, + List children, + int version ) { + super(uuid, parent, name, properties, children, version); + } + + public MockPathNode( UUID uuid, + Path parent, + Segment name, + Map properties, + List children ) { + super(uuid, parent, name, properties, children); + } + + public MockPathNode( UUID uuid ) { + super(uuid); + } + + @Override + public PathNode clone() { + return new MockPathNode(getUuid(), getParent(), getName(), new HashMap(getProperties()), + new ArrayList(getChildren())); + } + + @Override + public PathNode freeze() { + if (!hasChanges()) return this; + return new MockPathNode(getUuid(), changes.getParent(), changes.getName(), changes.getUnmodifiableProperties(), + changes.getUnmodifiableChildren(), getVersion() + 1); + } + +} Index: modeshape-graph/src/test/java/org/modeshape/graph/connector/base/MockPathRepository.java new file mode 100644 =================================================================== --- /dev/null (revision 1823) +++ modeshape-graph/src/test/java/org/modeshape/graph/connector/base/MockPathRepository.java (working copy) @@ -0,0 +1,64 @@ +package org.modeshape.graph.connector.base; + +import java.util.HashMap; +import java.util.Map; +import org.modeshape.graph.ExecutionContext; +import org.modeshape.graph.property.Path; +import org.modeshape.graph.property.Property; +import org.modeshape.graph.property.Path.Segment; +import org.modeshape.graph.request.InvalidWorkspaceException; + +public class MockPathRepository extends Repository { + + protected final Map workspaces = new HashMap(); + + public MockPathRepository( BaseRepositorySource source ) { + super(source); + initialize(); + } + + @Override + public PathTransaction startTransaction( ExecutionContext context, + boolean readonly ) { + return new MockPathTransaction(this); + } + + public class MockPathTransaction extends PathTransaction { + + @Override + protected MockPathNode createNode( Segment name, + Path parentPath, + Iterable properties ) { + return new MockPathNode(null, parentPath, name, properties, null); + } + + public MockPathTransaction( Repository repository ) { + super(repository, repository.getRootNodeUuid()); + } + + @Override + public boolean destroyWorkspace( MockPathWorkspace workspace ) throws InvalidWorkspaceException { + return false; + } + + @Override + public MockPathWorkspace getWorkspace( String name, + MockPathWorkspace originalToClone ) throws InvalidWorkspaceException { + MockPathWorkspace workspace = workspaces.get(name); + + if (workspace != null) { + return workspace; + } + + if (originalToClone != null) { + workspace = new MockPathWorkspace(name, originalToClone); + } else { + workspace = new MockPathWorkspace(name, getRepository().getRootNodeUuid()); + } + + workspaces.put(name, workspace); + return workspace; + } + + } +} Index: modeshape-graph/src/test/java/org/modeshape/graph/connector/base/MockPathWorkspace.java new file mode 100644 =================================================================== --- /dev/null (revision 1823) +++ modeshape-graph/src/test/java/org/modeshape/graph/connector/base/MockPathWorkspace.java (working copy) @@ -0,0 +1,217 @@ +package org.modeshape.graph.connector.base; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import org.modeshape.graph.ExecutionContext; +import org.modeshape.graph.property.Name; +import org.modeshape.graph.property.Path; +import org.modeshape.graph.property.PathFactory; +import org.modeshape.graph.property.Property; +import org.modeshape.graph.property.Path.Segment; + +public class MockPathWorkspace extends PathWorkspace { + + protected ExecutionContext context = new ExecutionContext(); + protected InternalNode rootNode; + + public MockPathWorkspace( String name, + UUID rootNodeUuid ) { + super(name, rootNodeUuid); + } + + public MockPathWorkspace( String name, + MockPathWorkspace originalToClone ) { + super(name, originalToClone); + } + + protected PathFactory pathFactory() { + return context.getValueFactories().getPathFactory(); + } + + private InternalNode nodeAt( Path path ) { + InternalNode node = rootNode(); + + for (Segment segment : path) { + node = node.getChild(segment); + if (node == null) return null; + } + return node; + } + + @Override + public MockPathNode getNode( Path path ) { + + if (nodeAt(path) == null) { + assert nodeAt(path) != null; + } + + return pathNodeFor(nodeAt(path)); + } + + private InternalNode rootNode() { + if (rootNode == null) { + rootNode = new InternalNode(getRootNodeUuid()); + } + + return rootNode; + } + + @Override + public MockPathNode getRootNode() { + return pathNodeFor(rootNode()); + } + + private MockPathNode pathNodeFor(InternalNode node) { + if (node.getParent() == null) { + new MockPathNode(node.getUuid(), null, node.getName(), node.getProperties(), node.getChildren()); + } + return new MockPathNode(node.getUuid(), node.getPath().getParent(), node.getName(), node.getProperties(), + node.getChildren()); + } + + @Override + public MockPathNode putNode( MockPathNode node ) { + InternalNode target; + if (node.getParent() == null) { + target = rootNode; + } + else { + InternalNode parent = nodeAt(node.getParent()); + target = parent.getChild(node.getName()); + + if (target == null) { + target = new InternalNode(parent, node.getName(), node.getProperties(), null); + parent.addChild(target); + return pathNodeFor(target); + } + } + + target.setProperties(node.getProperties()); + + return pathNodeFor(target); + } + + @Override + public MockPathNode moveNode( MockPathNode node, + MockPathNode newNode ) { + InternalNode parent = nodeAt(newNode.getParent()); + + InternalNode child = nodeAt(pathFactory().create(node.getParent(), node.getName())); + + parent.addChild(child); + + return pathNodeFor(child); + } + + @Override + public void removeAll() { + rootNode = null; + } + + @Override + public MockPathNode removeNode( Path path ) { + if (path.isRoot()) { + InternalNode oldRoot = rootNode; + removeAll(); + return pathNodeFor(oldRoot); + } + + InternalNode target = nodeAt(path); + InternalNode parent = target.getParent(); + + parent.removeChild(target.getName()); + + return pathNodeFor(target); + } + + private class InternalNode { + private UUID uuid; + private Segment name; + private InternalNode parent; + private Map properties; + private List children; + + protected InternalNode( UUID uuid ) { + this(uuid, null, null, null, null); + } + + protected InternalNode( InternalNode parent, + Segment name, + Map properties, + List children ) { + this(null, parent, name, properties, children); + } + + protected InternalNode( UUID uuid, + InternalNode parent, + Segment name, + Map properties, + List children ) { + this.uuid = uuid; + this.parent = parent; + this.name = name; + this.properties = properties == null ? new HashMap() : new HashMap(properties); + this.children = children == null ? new ArrayList() : children; + } + + protected InternalNode getChild( Segment segment ) { + for (InternalNode node : children) { + if (node.getName().equals(segment)) return node; + } + return null; + } + + protected void removeChild( Segment segment ) { + children.remove(segment); + } + + protected void addChild( InternalNode child ) { + children.add(child); + child.parent = this; + } + + protected List getChildren() { + List childSegments = new ArrayList(children.size()); + + for (InternalNode child : children) { + childSegments.add(child.getName()); + } + + return childSegments; + } + + protected Map getProperties() { + return new HashMap(properties); + } + + protected void setProperties( Map properties ) { + this.properties = new HashMap(properties); + } + + protected InternalNode getParent() { + return parent; + } + + protected Segment getName() { + return name; + } + + protected UUID getUuid() { + return uuid; + } + + protected Path getPath() { + if (parent == null) return pathFactory().createRootPath(); + + return pathFactory().create(parent.getPath(), name); + } + + @Override + public String toString() { + return getPath().getString(context.getNamespaceRegistry()); + } + } +} Index: modeshape-graph/src/test/java/org/modeshape/graph/connector/base/PathRepositoryTest.java new file mode 100644 =================================================================== --- /dev/null (revision 1823) +++ modeshape-graph/src/test/java/org/modeshape/graph/connector/base/PathRepositoryTest.java (working copy) @@ -0,0 +1,328 @@ +package org.modeshape.graph.connector.base; + +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.hamcrest.CoreMatchers.nullValue; +import static org.hamcrest.core.Is.is; +import static org.junit.Assert.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; +import java.util.UUID; +import org.junit.Before; +import org.junit.Test; +import org.modeshape.graph.ExecutionContext; +import org.modeshape.graph.Location; +import org.modeshape.graph.ModeShapeLexicon; +import org.modeshape.graph.connector.RepositoryContext; +import org.modeshape.graph.connector.base.MockPathRepository.MockPathTransaction; +import org.modeshape.graph.property.Name; +import org.modeshape.graph.property.PathFactory; +import org.modeshape.graph.property.Property; +import org.modeshape.graph.property.Path.Segment; +import org.modeshape.graph.request.CreateWorkspaceRequest.CreateConflictBehavior; + +public class PathRepositoryTest { + + private ExecutionContext context; + private UUID rootNodeUuid; + private String sourceName; + private String defaultWorkspaceName; + private Repository repository; + private BaseRepositorySource source; + + @Before + public void beforeEach() throws Exception { + context = new ExecutionContext(); + rootNodeUuid = UUID.randomUUID(); + sourceName = "Path Source"; + defaultWorkspaceName = "default"; + + RepositoryContext repositoryContext = mock(RepositoryContext.class); + when(repositoryContext.getExecutionContext()).thenReturn(context); + + source = mock(BaseRepositorySource.class); + when(source.areUpdatesAllowed()).thenReturn(true); + when(source.getRootNodeUuidObject()).thenReturn(rootNodeUuid); + when(source.getDefaultWorkspaceName()).thenReturn(defaultWorkspaceName); + when(source.getName()).thenReturn(sourceName); + when(source.getRepositoryContext()).thenReturn(repositoryContext); + + repository = new MockPathRepository(source); + } + + private MockPathTransaction beginTransaction() { + return (MockPathTransaction)repository.startTransaction(context, false); + } + + private MockPathWorkspace defaultWorkspaceFor( MockPathTransaction txn ) { + return txn.getWorkspace(defaultWorkspaceName, null); + } + + private Name name( String name ) { + return context.getValueFactories().getNameFactory().create(name); + } + + private Segment segment( String name ) { + return context.getValueFactories().getPathFactory().createSegment(name); + } + + private PathFactory pathFactory() { + return context.getValueFactories().getPathFactory(); + } + + @Test + public void shouldHaveDefaultWorkspace() { + assertThat(repository.getWorkspaceNames(), is(Collections.singleton(defaultWorkspaceName))); + } + + @Test + public void shouldBeAbleToStartTransaction() { + assertThat(repository.startTransaction(context, false), is(notNullValue())); + } + + @Test + public void shouldBeAbleToCreateWorkspaceWithNoClone() { + MockPathTransaction txn = beginTransaction(); + + String newWorkspaceName = "new workspace"; + repository.createWorkspace(txn, newWorkspaceName, CreateConflictBehavior.DO_NOT_CREATE, null); + + txn.commit(); + + Set workspaceNames = new HashSet(Arrays.asList(new String[] {defaultWorkspaceName, newWorkspaceName})); + assertThat(repository.getWorkspaceNames(), is(workspaceNames)); + } + + @Test + public void shouldBeAbleToReadRootNode() { + MockPathTransaction txn = beginTransaction(); + MockPathWorkspace workspace = defaultWorkspaceFor(txn); + + Location location = Location.create(rootNodeUuid); + + PathNode rootNode = txn.getNode(workspace, location); + assertThat(rootNode, is(notNullValue())); + assertThat(rootNode.getParent(), is(nullValue())); + assertThat(rootNode.getUuid(), is(rootNodeUuid)); + assertThat(rootNode.getName(), is(nullValue())); + } + + private Property property( Name name, + Object... values ) { + return context.getPropertyFactory().create(name, values); + } + + @Test + public void shouldBeAbleToSetPropertyOnRootNode() { + MockPathTransaction txn = beginTransaction(); + MockPathWorkspace workspace = defaultWorkspaceFor(txn); + + Location location = Location.create(rootNodeUuid); + MockPathNode rootNode = txn.getNode(workspace, location); + + Property property = property(ModeShapeLexicon.ROOT, true); + rootNode = txn.setProperties(workspace, rootNode, Collections.singleton(property), null, false); + assertThat(rootNode.getProperty(ModeShapeLexicon.ROOT), is(notNullValue())); + txn.commit(); + + txn = beginTransaction(); + workspace = defaultWorkspaceFor(txn); + + rootNode = txn.getNode(workspace, location); + assertThat(rootNode.getProperty(ModeShapeLexicon.ROOT), is(notNullValue())); + } + + @Test + public void shouldBeAbleToAddChildToRootNode() { + MockPathTransaction txn = beginTransaction(); + MockPathWorkspace workspace = defaultWorkspaceFor(txn); + + Location location = Location.create(rootNodeUuid); + MockPathNode rootNode = txn.getNode(workspace, location); + + PathNode newNode = txn.addChild(workspace, rootNode, name("child"), -1, null, null); + + rootNode = txn.getNode(workspace, Location.create(newNode.getParent())); + assertThat(rootNode.getChildren().contains(segment("child")), is(true)); + assertThat(newNode.getParent().isRoot(), is(true)); + txn.commit(); + + txn = beginTransaction(); + workspace = defaultWorkspaceFor(txn); + + rootNode = txn.getNode(workspace, location); + newNode = txn.getNode(workspace, Location.create(pathFactory().create("/child"))); + assertThat(newNode, is(notNullValue())); + assertThat(newNode.getParent(), is(pathFactory().createRootPath())); + } + + @Test + public void shouldBeAbleToCopyChildToNewParent() { + MockPathTransaction txn = beginTransaction(); + MockPathWorkspace workspace = defaultWorkspaceFor(txn); + + Location location = Location.create(rootNodeUuid); + MockPathNode rootNode = txn.getNode(workspace, location); + + /* + * Building this graph: + * /a + * /b + * /c + * /new + */ + + MockPathNode aNode = txn.addChild(workspace, rootNode, name("a"), -1, null, null); + MockPathNode bNode = txn.addChild(workspace, aNode, name("b"), -1, null, null); + MockPathNode cNode = txn.addChild(workspace, bNode, name("c"), -1, null, null); + + MockPathNode newNode = txn.addChild(workspace, rootNode, name("new"), -1, null, null); + + rootNode = txn.getNode(workspace, Location.create(newNode.getParent())); + assertThat(rootNode.getChildren().contains(segment("a")), is(true)); + assertThat(rootNode.getChildren().contains(segment("new")), is(true)); + assertThat(cNode.getParent(), is(pathFactory().create("/a/b"))); + assertThat(txn.getNode(workspace, Location.create(pathFactory().create("/a/b/c"))), is(notNullValue())); + + // Have to refresh bNode after the child is added + bNode = txn.getNode(workspace, Location.create(pathFactory().create("/a/b"))); + MockPathNode newBNode = txn.copyNode(workspace, bNode, workspace, newNode, null, true); + newNode = txn.getNode(workspace, Location.create(newBNode.getParent())); + + assertThat(rootNode.getChildren().contains(segment("a")), is(true)); + assertThat(rootNode.getChildren().contains(segment("new")), is(true)); + assertThat(newNode.getChildren().contains(segment("b")), is(true)); + assertThat(newBNode.getChildren().contains(segment("c")), is(true)); + + txn.commit(); + + txn = beginTransaction(); + workspace = defaultWorkspaceFor(txn); + + rootNode = txn.getNode(workspace, location); + newNode = txn.getNode(workspace, Location.create(pathFactory().create("/new"))); + assertThat(newNode, is(notNullValue())); + assertThat(newNode.getParent(), is(pathFactory().createRootPath())); + } + + @Test + public void shouldBeAbleToMoveChildToNewParent() { + MockPathTransaction txn = beginTransaction(); + MockPathWorkspace workspace = defaultWorkspaceFor(txn); + + Location location = Location.create(rootNodeUuid); + MockPathNode rootNode = txn.getNode(workspace, location); + + /* + * Building this graph: + * /a + * /b + * /c + * /new + */ + + MockPathNode aNode = txn.addChild(workspace, rootNode, name("a"), -1, null, null); + MockPathNode bNode = txn.addChild(workspace, aNode, name("b"), -1, null, null); + MockPathNode cNode = txn.addChild(workspace, bNode, name("c"), -1, null, null); + + MockPathNode newNode = txn.addChild(workspace, rootNode, name("new"), -1, null, null); + + rootNode = txn.getNode(workspace, Location.create(newNode.getParent())); + assertThat(rootNode.getChildren().contains(segment("a")), is(true)); + assertThat(rootNode.getChildren().contains(segment("new")), is(true)); + assertThat(cNode.getParent(), is(pathFactory().create("/a/b"))); + assertThat(txn.getNode(workspace, Location.create(pathFactory().create("/a/b/c"))), is(notNullValue())); + + txn.commit(); + + txn = beginTransaction(); + + // Have to refresh bNode after the child is added + bNode = txn.getNode(workspace, Location.create(pathFactory().create("/a/b"))); + Location newBLocation = txn.addChild(workspace, newNode, bNode, null, null); + MockPathNode newBNode = txn.getNode(workspace, newBLocation); + newNode = txn.getNode(workspace, Location.create(pathFactory().create("/new"))); + + cNode = txn.getNode(workspace, Location.create(pathFactory().create("/new/b/c"))); + assertThat(cNode.getParent(), is(pathFactory().create("/new/b"))); + + assertThat(rootNode.getChildren().contains(segment("a")), is(true)); + assertThat(rootNode.getChildren().contains(segment("new")), is(true)); + assertThat(newNode.getChildren().contains(segment("b")), is(true)); + assertThat(newBNode.getChildren().contains(segment("c")), is(true)); + + txn.commit(); + + txn = beginTransaction(); + workspace = defaultWorkspaceFor(txn); + + rootNode = txn.getNode(workspace, location); + newNode = txn.getNode(workspace, Location.create(pathFactory().create("/new"))); + assertThat(newNode, is(notNullValue())); + assertThat(newNode.getParent(), is(pathFactory().createRootPath())); + } + + @Test + public void shouldBeAbleToStackMoves() { + MockPathTransaction txn = beginTransaction(); + MockPathWorkspace workspace = defaultWorkspaceFor(txn); + + Location location = Location.create(rootNodeUuid); + MockPathNode rootNode = txn.getNode(workspace, location); + + /* + * Building this graph: + * /a + * /b + * /c + * /new + * /d + * /e + */ + + MockPathNode aNode = txn.addChild(workspace, rootNode, name("a"), -1, null, null); + MockPathNode bNode = txn.addChild(workspace, aNode, name("b"), -1, null, null); + MockPathNode cNode = txn.addChild(workspace, bNode, name("c"), -1, null, null); + MockPathNode newNode = txn.addChild(workspace, rootNode, name("new"), -1, null, null); + MockPathNode dNode = txn.addChild(workspace, rootNode, name("d"), -1, null, null); + MockPathNode eNode = txn.addChild(workspace, dNode, name("e"), -1, null, null); + + txn.commit(); + + txn = beginTransaction(); + + bNode = txn.getNode(workspace, Location.create(pathFactory().create("/a/b"))); + Location newBLocation = txn.addChild(workspace, newNode, bNode, null, null); + + MockPathNode newBNode = txn.getNode(workspace, newBLocation); + newNode = txn.getNode(workspace, Location.create(pathFactory().create("/new"))); + + cNode = txn.getNode(workspace, Location.create(pathFactory().create("/new/b/c"))); + dNode = txn.getNode(workspace, Location.create(pathFactory().create("/d"))); + Location newDLocation = txn.addChild(workspace, cNode, dNode, null, null); + txn.getNode(workspace, newDLocation); + + eNode = txn.getNode(workspace, Location.create(pathFactory().create("/new/b/c/d/e"))); + assertThat(eNode.getParent(), is(pathFactory().create("/new/b/c/d"))); + + + rootNode = txn.getNode(workspace, location); + assertThat(rootNode.getChildren().contains(segment("new")), is(true)); + assertThat(newNode.getChildren().contains(segment("b")), is(true)); + assertThat(newBNode.getChildren().contains(segment("c")), is(true)); + + txn.commit(); + + txn = beginTransaction(); + workspace = defaultWorkspaceFor(txn); + + rootNode = txn.getNode(workspace, location); + newNode = txn.getNode(workspace, Location.create(pathFactory().create("/new"))); + assertThat(newNode, is(notNullValue())); + assertThat(newNode.getParent(), is(pathFactory().createRootPath())); + } + +}