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 1821) +++ extensions/modeshape-connector-filesystem/src/main/java/org/modeshape/connector/filesystem/FileSystemRepository.java (working copy) @@ -23,51 +23,14 @@ */ 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 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.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.connector.base.PathNode; +import org.modeshape.graph.connector.base.Repository; import org.modeshape.graph.request.InvalidWorkspaceException; -import org.modeshape.graph.request.Request; /** * Implementation of {@code WritablePathRepository} that provides access to an underlying file system. This repository only @@ -75,8 +38,7 @@ import org.modeshape.graph.request.Request; * {@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 +74,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 +92,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,521 +118,10 @@ public class FileSystemRepository extends WritablePathRepository { return directory; } - /** - * Writable workspace implementation for file system-backed workspaces - */ - public class FileSystemWorkspace extends AbstractWritablePathWorkspace { - - private final ExecutionContext context; - private final File workspaceRoot; - - public FileSystemWorkspace( String name, - ExecutionContext context, - File workspaceRoot ) { - super(name, source.getRootNodeUuid()); - this.workspaceRoot = workspaceRoot; - this.context = context; - } - - 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; - } - - 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); - } - - 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()); - - 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(); - } - } - // Shouldn't be able to get this far is path is truly invalid - return path; - } - - 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; - } - - /** - * 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; - } - - protected void ensureValidPathLength( File root ) { - ensureValidPathLength(root, 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(), - 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 FileSystemTransaction startTransaction( ExecutionContext context, + boolean readonly ) { + return new FileSystemTransaction(this, source.getRootNodeUuidObject()); } + } 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 1821) +++ extensions/modeshape-connector-filesystem/src/main/java/org/modeshape/connector/filesystem/FileSystemSource.java (working copy) @@ -52,11 +52,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 +66,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 +135,8 @@ public class FileSystemSource extends AbstractPathRepositorySource implements Ob private transient FileSystemRepository repository; private volatile CustomPropertiesFactory customPropertiesFactory; + private ExecutionContext defaultContext = new ExecutionContext(); + /** * */ @@ -527,8 +531,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/test/java/org/modeshape/connector/filesystem/FileSystemConnectorCreateWorkspacesTest.java =================================================================== --- extensions/modeshape-connector-filesystem/src/test/java/org/modeshape/connector/filesystem/FileSystemConnectorCreateWorkspacesTest.java (revision 1821) +++ 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: 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 1821) +++ modeshape-graph/src/main/java/org/modeshape/graph/connector/base/MapTransaction.java (working copy) @@ -91,6 +91,15 @@ public abstract class MapTransaction getWorkspaceNames() { + return repository.getWorkspaceNames(); + } + + /** * 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()}. Index: modeshape-graph/src/main/java/org/modeshape/graph/connector/base/PathNode.java new file mode 100644 =================================================================== --- /dev/null (revision 1821) +++ modeshape-graph/src/main/java/org/modeshape/graph/connector/base/PathNode.java (working copy) @@ -0,0 +1,609 @@ +/* + * 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.io.Serializable; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedList; +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; + +/** + * A {@link Node} implementation used by the map-based connector (see {@link MapWorkspace} and {@link MapTransaction}), which + * stores all node state in a map. + *

+ * 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 MapTransaction} 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 of 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 of 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 of 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(); + sb.append(this.getParent()).append("/"); + sb.append(this.getName()).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() { + try { + PathNode node = (PathNode)super.clone(); + node.uuid = uuid; + node.parent = parent; + node.name = name; + node.properties = new HashMap(properties); + node.children = new ArrayList(children); + return node; + } catch (CloneNotSupportedException cnse) { + throw new IllegalStateException(cnse); + } + } + + /** + * 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) { + // if (JcrLexicon.UUID.equals(name) || ModeShapeLexicon.UUID.equals(name)) continue; + 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 with 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; + } + + 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 1821) +++ modeshape-graph/src/main/java/org/modeshape/graph/connector/base/PathTransaction.java (working copy) @@ -0,0 +1,832 @@ +/* + * 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 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; + + /** + * 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(), 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; + } + + /** + * {@inheritDoc} + * + * @see org.modeshape.graph.connector.base.Transaction#getWorkspaceNames() + */ + public Set getWorkspaceNames() { + return repository.getWorkspaceNames(); + } + + /** + * 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 (repository.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)); + } + + private Location locationFor( NodeType node ) { + return Location.create(pathTo(node)); + } + + private 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.changed(newChild); + // changes.changed(parent); + changes.moved(newChildWithOldParent, newChild, parent); + + return locationFor(newChild); + } + + /** + * Create a new instance of the node, given the supplied UUID. This method should do nothing but instantiate the new node; the + * caller will add to the appropriate maps. + * + * @param name the name of the new node; may be null if the name is not known + * @param parentUuid the UUID 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; + } + + /** + * {@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 ) { + Path path = pathTo(node); + removedNodes.remove(path); + changedOrAddedNodes.put(path, node); + commands.add(workspace.createPutCommand(node)); + } + + public void changed( NodeType 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 ) { + 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() { + boolean prepareSucceeded = true; + + try { + workspace.beginCommit(); + + for (ChangeCommand command : commands) { + prepareSucceeded &= command.prepare(); + if (!prepareSucceeded) { + break; + } + } + + for (ChangeCommand command : commands) { + if (prepareSucceeded) { + command.apply(); + } + else { + command.rollback(); + } + } + } + finally { + workspace.endCommit(); + } + } + + @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 1821) +++ modeshape-graph/src/main/java/org/modeshape/graph/connector/base/PathWorkspace.java (working copy) @@ -0,0 +1,248 @@ +/* + * 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.Map; +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 MapNode} objects and stores them within a {@link Map} + * keyed by their UUID. + *

+ * Subclasses are required to provide thread-safe access and modification of the state within the encapsulated map, since multiple + * {@link Transaction} implementations may be {@link Transaction#commit() committing} changes to the map 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 map 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 + * + * @param node the new node; may not be null + * @return the previous node state, or null if the node is new to this workspace + */ + public NodeType putNode( NodeType node ) { + throw new UnsupportedOperationException(); + } + + public NodeType moveNode( NodeType node, + NodeType newParent ) { + throw new UnsupportedOperationException(); + } + + /** + * Remove and return the node with the supplied path. This method will never remove the root node. + * + * @param path the path to the node + * @return the node that was removed, or null if the supplied path is the root path or if this workspace does not contain a + * node at the supplied 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; + } + + public void beginCommit() { + + } + + public void endCommit() { + + } + + public ChangeCommand createPutCommand( NodeType node ) { + return new PutCommand(node); + } + + public ChangeCommand createRemoveCommand( Path path ) { + return new RemoveCommand(path); + } + + public ChangeCommand createMoveCommand( NodeType node, + NodeType newNode ) { + return new MoveCommand(node, newNode); + } + + public interface ChangeCommand { + boolean prepare(); + + void rollback(); + void apply(); + } + + public abstract class BaseChangeCommand implements ChangeCommand { + public boolean prepare() { + return true; + } + + public void rollback() { + + } + + } + + private class PutCommand extends BaseChangeCommand { + private NodeType node; + + private PutCommand( NodeType node ) { + super(); + this.node = node; + } + + public void apply() { + PathWorkspace.this.putNode(node); + } + + @Override + public String toString() { + return "Put: { " + node + "}"; + } + } + + private class RemoveCommand extends BaseChangeCommand { + private Path path; + + private 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 extends BaseChangeCommand { + private NodeType node; + private NodeType newNode; + + private 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 1821) +++ 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 1821) +++ 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 1821) +++ 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 1821) +++ modeshape-graph/src/test/java/org/modeshape/graph/connector/base/MockPathNode.java (working copy) @@ -0,0 +1,50 @@ +package org.modeshape.graph.connector.base; + +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 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 1821) +++ 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 { + + private 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 1821) +++ modeshape-graph/src/test/java/org/modeshape/graph/connector/base/MockPathWorkspace.java (working copy) @@ -0,0 +1,216 @@ +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 { + + private ExecutionContext context = new ExecutionContext(); + private InternalNode rootNode; + + public MockPathWorkspace( String name, + UUID rootNodeUuid ) { + super(name, rootNodeUuid); + } + + public MockPathWorkspace( String name, + MockPathWorkspace originalToClone ) { + super(name, originalToClone); + } + + private 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.properties, 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; + + private InternalNode( UUID uuid ) { + this(uuid, null, null, null, null); + } + + private InternalNode( InternalNode parent, + Segment name, + Map properties, + List children ) { + this(null, parent, name, properties, children); + } + private 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; + } + + private InternalNode getChild( Segment segment ) { + for (InternalNode node : children) { + if (node.getName().equals(segment)) return node; + } + return null; + } + + private void removeChild( Segment segment ) { + children.remove(segment); + } + + private void addChild( InternalNode child ) { + children.add(child); + child.parent = this; + } + + private List getChildren() { + List childSegments = new ArrayList(children.size()); + + for (InternalNode child : children) { + childSegments.add(child.getName()); + } + + return childSegments; + } + + private Map getProperties() { + return new HashMap(properties); + } + + private void setProperties( Map properties ) { + this.properties = new HashMap(properties); + } + + private InternalNode getParent() { + return parent; + } + + private Segment getName() { + return name; + } + + private UUID getUuid() { + return uuid; + } + + private 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 1821) +++ 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())); + } + +}