Index: extensions/modeshape-connector-filesystem/src/main/java/org/modeshape/connector/filesystem/FileSystemI18n.java
===================================================================
--- extensions/modeshape-connector-filesystem/src/main/java/org/modeshape/connector/filesystem/FileSystemI18n.java (revision 1823)
+++ extensions/modeshape-connector-filesystem/src/main/java/org/modeshape/connector/filesystem/FileSystemI18n.java (working copy)
@@ -38,7 +38,6 @@ public final class FileSystemI18n {
public static I18n pathForWorkspaceRootIsNotDirectory;
public static I18n pathForWorkspaceRootCannotBeRead;
public static I18n propertyIsRequired;
- public static I18n locationInRequestMustHavePath;
public static I18n sameNameSiblingsAreNotAllowed;
public static I18n nodeOrderingNotSupported;
public static I18n onlyTheDefaultNamespaceIsAllowed;
@@ -63,7 +62,6 @@ public final class FileSystemI18n {
public static I18n couldNotUpdateData;
public static I18n missingRequiredProperty;
public static I18n deleteFailed;
- public static I18n copyFailed;
public static I18n getCanonicalPathFailed;
public static I18n maxPathLengthExceeded;
Index: extensions/modeshape-connector-filesystem/src/main/java/org/modeshape/connector/filesystem/FileSystemRepository.java
===================================================================
--- extensions/modeshape-connector-filesystem/src/main/java/org/modeshape/connector/filesystem/FileSystemRepository.java (revision 1823)
+++ extensions/modeshape-connector-filesystem/src/main/java/org/modeshape/connector/filesystem/FileSystemRepository.java (working copy)
@@ -23,60 +23,35 @@
*/
package org.modeshape.connector.filesystem;
-import java.io.BufferedInputStream;
import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
+import java.util.LinkedList;
+import java.util.UUID;
import org.modeshape.common.i18n.I18n;
-import org.modeshape.common.util.FileUtil;
-import org.modeshape.common.util.IoUtil;
import org.modeshape.graph.ExecutionContext;
-import org.modeshape.graph.JcrLexicon;
import org.modeshape.graph.JcrNtLexicon;
-import org.modeshape.graph.Location;
import org.modeshape.graph.ModeShapeLexicon;
-import org.modeshape.graph.NodeConflictBehavior;
-import org.modeshape.graph.connector.RepositorySourceException;
-import org.modeshape.graph.connector.path.AbstractWritablePathWorkspace;
-import org.modeshape.graph.connector.path.DefaultPathNode;
-import org.modeshape.graph.connector.path.PathNode;
-import org.modeshape.graph.connector.path.WritablePathRepository;
-import org.modeshape.graph.connector.path.WritablePathWorkspace;
-import org.modeshape.graph.connector.path.cache.WorkspaceCache;
-import org.modeshape.graph.mimetype.MimeTypeDetector;
-import org.modeshape.graph.property.Binary;
-import org.modeshape.graph.property.BinaryFactory;
-import org.modeshape.graph.property.DateTimeFactory;
-import org.modeshape.graph.property.Name;
-import org.modeshape.graph.property.NameFactory;
-import org.modeshape.graph.property.NamespaceRegistry;
+import org.modeshape.graph.connector.RepositoryContext;
+import org.modeshape.graph.connector.base.PathNode;
+import org.modeshape.graph.connector.base.PathTransaction;
+import org.modeshape.graph.connector.base.Processor;
+import org.modeshape.graph.connector.base.Repository;
+import org.modeshape.graph.connector.base.Transaction;
+import org.modeshape.graph.observe.Observer;
import org.modeshape.graph.property.Path;
-import org.modeshape.graph.property.PathFactory;
-import org.modeshape.graph.property.PathNotFoundException;
import org.modeshape.graph.property.Property;
-import org.modeshape.graph.property.PropertyFactory;
import org.modeshape.graph.property.Path.Segment;
import org.modeshape.graph.request.InvalidRequestException;
import org.modeshape.graph.request.InvalidWorkspaceException;
-import org.modeshape.graph.request.Request;
+import org.modeshape.graph.request.MoveBranchRequest;
+import org.modeshape.graph.request.processor.RequestProcessor;
/**
- * Implementation of {@code WritablePathRepository} that provides access to an underlying file system. This repository only
- * natively supports nodes of primary types {@link JcrNtLexicon#FOLDER nt:folder}, {@link JcrNtLexicon#FILE nt:file}, and
+ * Implementation of {@code Repository} that provides access to an underlying file system. This repository only natively supports
+ * nodes of primary types {@link JcrNtLexicon#FOLDER nt:folder}, {@link JcrNtLexicon#FILE nt:file}, and
* {@link ModeShapeLexicon#RESOURCE mode:resource}, although the {@link CustomPropertiesFactory} allows for the addition of mixin
* types to any and all primary types.
*/
-public class FileSystemRepository extends WritablePathRepository {
- private static final String DEFAULT_MIME_TYPE = "application/octet";
+public class FileSystemRepository extends Repository {
protected final FileSystemSource source;
private File repositoryRoot;
@@ -112,51 +87,8 @@ public class FileSystemRepository extends WritablePathRepository {
}
}
- if (!this.workspaces.isEmpty()) return;
-
- String defaultWorkspaceName = getDefaultWorkspaceName();
- ExecutionContext context = source.getRepositoryContext().getExecutionContext();
-
- for (String workspaceName : source.getPredefinedWorkspaceNames()) {
- doCreateWorkspace(context, workspaceName);
-
- }
-
- if (!workspaces.containsKey(defaultWorkspaceName)) {
- doCreateWorkspace(context, defaultWorkspaceName);
- }
- }
-
- public WorkspaceCache getCache( String workspaceName ) {
- return source.getPathRepositoryCache().getCache(workspaceName);
- }
-
- /**
- * Internal method that creates a workspace and adds it to the map of active workspaces without checking to see if
- * {@link FileSystemSource#isCreatingWorkspacesAllowed() the source allows creating workspaces}. This is useful when setting
- * up predefined workspaces.
- *
- * @param context the current execution context; may not be null
- * @param name the name of the workspace to create; may not be null
- * @return the newly created workspace; never null
- */
- private WritablePathWorkspace doCreateWorkspace( ExecutionContext context,
- String name ) {
- File directory = getWorkspaceDirectory(name);
-
- FileSystemWorkspace workspace = new FileSystemWorkspace(name, context, directory);
-
- workspaces.putIfAbsent(name, workspace);
- return (WritablePathWorkspace)workspaces.get(name);
-
+ super.initialize();
}
-
- /**
- * Creates directory and any missing parent directories
- *
- * @param directory the directory to create
- * @throws RepositorySourceException if the directory or one of its parents cannot be created
- */
private void createDirectory( File directory ) {
File parent = directory.getParentFile();
@@ -173,17 +105,6 @@ public class FileSystemRepository extends WritablePathRepository {
}
}
- @Override
- protected WritablePathWorkspace createWorkspace( ExecutionContext context,
- String name ) {
- if (!source.isCreatingWorkspacesAllowed()) {
- String msg = FileSystemI18n.unableToCreateWorkspaces.text(getSourceName(), name);
- throw new InvalidRequestException(msg);
- }
-
- return doCreateWorkspace(context, name);
- }
-
/**
* @param workspaceName the name of the workspace for which the root directory should be returned
* @return the directory that maps to the root node in the named workspace; may be null if the directory does not exist, is a
@@ -210,520 +131,79 @@ public class FileSystemRepository extends WritablePathRepository {
return directory;
}
+ @Override
+ public FileSystemTransaction startTransaction( ExecutionContext context,
+ boolean readonly ) {
+ return new FileSystemTransaction(this, source.getRootNodeUuidObject());
+ }
+
+ @Override
+ public RequestProcessor createRequestProcessor( Transaction txn ) {
+ RepositoryContext repositoryContext = this.source.getRepositoryContext();
+ Observer observer = repositoryContext != null ? repositoryContext.getObserver() : null;
+ return new FileSystemProcessor(txn, this, observer, source.areUpdatesAllowed());
+ }
+
/**
- * Writable workspace implementation for file system-backed workspaces
+ * Implementation of the {@link PathTransaction} interface for the file system connector
*/
- public class FileSystemWorkspace extends AbstractWritablePathWorkspace {
-
- private final ExecutionContext context;
- private final File workspaceRoot;
+ class FileSystemTransaction extends PathTransaction {
- public FileSystemWorkspace( String name,
- ExecutionContext context,
- File workspaceRoot ) {
- super(name, source.getRootNodeUuid());
- this.workspaceRoot = workspaceRoot;
- this.context = context;
+ public FileSystemTransaction( FileSystemRepository repository,
+ UUID rootNodeUuid ) {
+ super(repository, rootNodeUuid);
}
- public PathNode createNode( ExecutionContext context,
- PathNode parentNode,
- Name name,
- Map properties,
- NodeConflictBehavior conflictBehavior ) {
- NameFactory nameFactory = context.getValueFactories().getNameFactory();
- PathFactory pathFactory = context.getValueFactories().getPathFactory();
- NamespaceRegistry registry = context.getNamespaceRegistry();
- /*
- * Get references to java.io.Files
- */
-
- Path parentPath = parentNode.getPath();
- File parentFile = fileFor(parentPath);
-
- Path newPath = pathFactory.create(parentPath, name);
- String newName = name.getString(registry);
- File newFile = new File(parentFile, newName);
-
- /*
- * Determine the node primary type
- */
- Property primaryTypeProp = properties.get(JcrLexicon.PRIMARY_TYPE);
-
- // Default primary type to nt:folder
- Name primaryType = primaryTypeProp == null ? JcrNtLexicon.FOLDER : nameFactory.create(primaryTypeProp.getFirstValue());
- CustomPropertiesFactory customPropertiesFactory = source.customPropertiesFactory();
-
- if (JcrNtLexicon.FILE.equals(primaryType)) {
-
- // The FILE node is represented by the existence of the file
- if (!parentFile.canWrite()) {
- I18n msg = FileSystemI18n.parentIsReadOnly;
- throw new RepositorySourceException(getSourceName(), msg.text(parentPath, this.getName(), getSourceName()));
- }
-
- try {
- ensureValidPathLength(newFile);
- boolean skipWrite = false;
-
- if (newFile.exists()) {
- if (conflictBehavior.equals(NodeConflictBehavior.APPEND)) {
- I18n msg = FileSystemI18n.sameNameSiblingsAreNotAllowed;
- throw new InvalidRequestException(msg.text(getSourceName(), newName));
- } else if (conflictBehavior.equals(NodeConflictBehavior.DO_NOT_REPLACE)) {
- skipWrite = true;
- }
- }
-
- // Don't try to write if the node conflict behavior is DO_NOT_REPLACE
- if (!skipWrite) {
- if (!newFile.createNewFile()) {
- I18n msg = FileSystemI18n.fileAlreadyExists;
- throw new RepositorySourceException(getSourceName(), msg.text(parentPath, getName(), getSourceName()));
- }
- }
- } catch (IOException ioe) {
- I18n msg = FileSystemI18n.couldNotCreateFile;
- throw new RepositorySourceException(getSourceName(), msg.text(parentPath,
- getName(),
- getSourceName(),
- ioe.getMessage()), ioe);
- }
-
- customPropertiesFactory.recordFileProperties(context,
- getSourceName(),
- Location.create(newPath),
- newFile,
- properties);
- } else if (JcrNtLexicon.RESOURCE.equals(primaryType) || ModeShapeLexicon.RESOURCE.equals(primaryType)) {
- if (!JcrLexicon.CONTENT.equals(name)) {
- I18n msg = FileSystemI18n.invalidNameForResource;
- String nodeName = name.getString();
- throw new RepositorySourceException(getSourceName(), msg.text(parentPath,
- getName(),
- getSourceName(),
- nodeName));
- }
-
- if (!parentFile.isFile()) {
- I18n msg = FileSystemI18n.invalidPathForResource;
- throw new RepositorySourceException(getSourceName(), msg.text(parentPath, getName(), getSourceName()));
- }
-
- if (!parentFile.canWrite()) {
- I18n msg = FileSystemI18n.parentIsReadOnly;
- throw new RepositorySourceException(getSourceName(), msg.text(parentPath, getName(), getSourceName()));
- }
-
- boolean updateFileContent = true;
- switch (conflictBehavior) {
- case APPEND:
- case REPLACE:
- case UPDATE:
- // When the "nt:file" parent node was created, it automatically creates the "jcr:content"
- // child node with empty content. Therefore, creating a new "jcr:content" node is
- // not technically possible (recall that same-name-siblings are not supported in general,
- // but certainly not for the "jcr:content" node). Therefore, we can treat all these
- // conflict behavior cases as a simple update to the existing "jcr:content" child node.
- break;
- case DO_NOT_REPLACE:
- updateFileContent = false;
- }
-
- if (updateFileContent) {
- // Copy over data into a temp file, then move it to the correct location
- FileOutputStream fos = null;
- try {
- File temp = File.createTempFile("dna", null);
- fos = new FileOutputStream(temp);
-
- Property dataProp = properties.get(JcrLexicon.DATA);
- if (dataProp == null) {
- I18n msg = FileSystemI18n.missingRequiredProperty;
- String dataPropName = JcrLexicon.DATA.getString();
- throw new RepositorySourceException(getSourceName(), msg.text(parentPath,
- getName(),
- getSourceName(),
- dataPropName));
- }
-
- BinaryFactory binaryFactory = context.getValueFactories().getBinaryFactory();
- Binary binary = binaryFactory.create(properties.get(JcrLexicon.DATA).getFirstValue());
-
- IoUtil.write(binary.getStream(), fos);
-
- if (!FileUtil.delete(parentFile)) {
- I18n msg = FileSystemI18n.deleteFailed;
- throw new RepositorySourceException(getSourceName(), msg.text(parentPath, getName(), getSourceName()));
- }
-
- if (!temp.renameTo(parentFile)) {
- I18n msg = FileSystemI18n.couldNotUpdateData;
- throw new RepositorySourceException(getSourceName(), msg.text(parentPath, getName(), getSourceName()));
- }
- } catch (IOException ioe) {
- I18n msg = FileSystemI18n.couldNotWriteData;
- throw new RepositorySourceException(getSourceName(), msg.text(parentPath,
- getName(),
- getSourceName(),
- ioe.getMessage()), ioe);
-
- } finally {
- try {
- if (fos != null) fos.close();
- } catch (Exception ex) {
- }
- }
- }
- customPropertiesFactory.recordResourceProperties(context,
- getSourceName(),
- Location.create(parentPath),
- newFile,
- properties);
-
- } else if (JcrNtLexicon.FOLDER.equals(primaryType) || primaryType == null) {
- ensureValidPathLength(newFile);
-
- if (!newFile.mkdir()) {
- I18n msg = FileSystemI18n.couldNotCreateFile;
- throw new RepositorySourceException(getSourceName(),
- msg.text(parentPath,
- getName(),
- getSourceName(),
- primaryType == null ? "null" : primaryType.getString(registry)));
- }
- customPropertiesFactory.recordDirectoryProperties(context,
- getSourceName(),
- Location.create(newPath),
- newFile,
- properties);
-
- } else {
- // Set error and return
- I18n msg = FileSystemI18n.unsupportedPrimaryType;
- throw new RepositorySourceException(getSourceName(), msg.text(primaryType.getString(registry),
- parentPath,
- getName(),
- getSourceName()));
- }
-
- PathNode node = getNode(newPath);
-
- List newChildren = new ArrayList(parentNode.getChildSegments().size() + 1);
- newChildren.addAll(parentNode.getChildSegments());
- newChildren.add(node.getPath().getLastSegment());
-
- WorkspaceCache cache = getCache(getName());
- cache.set(new DefaultPathNode(parentNode.getPath(), parentNode.getUuid(), parentNode.getProperties(), newChildren));
- cache.set(node);
-
- return node;
+ @Override
+ protected PathNode createNode( Segment name,
+ Path parentPath,
+ Iterable properties ) {
+ return new PathNode(null, parentPath, name, properties, new LinkedList());
}
- public boolean removeNode( ExecutionContext context,
- Path nodePath ) {
- File nodeFile;
-
- if (!nodePath.isRoot() && JcrLexicon.CONTENT.equals(nodePath.getLastSegment().getName())) {
- nodeFile = fileFor(nodePath.getParent());
- if (!nodeFile.exists()) return false;
-
- FileOutputStream fos = null;
- try {
- fos = new FileOutputStream(nodeFile);
- IoUtil.write("", fos);
- } catch (IOException ioe) {
- throw new RepositorySourceException(getSourceName(), FileSystemI18n.deleteFailed.text(nodePath,
- getName(),
- getSourceName()));
- } finally {
- if (fos != null) try {
- fos.close();
- } catch (IOException ioe) {
- }
- }
- } else {
- nodeFile = fileFor(nodePath);
- if (!nodeFile.exists()) return false;
-
- FileUtil.delete(nodeFile);
- }
-
+ @Override
+ public boolean destroyWorkspace( FileSystemWorkspace workspace ) throws InvalidWorkspaceException {
return true;
}
- public PathNode setProperties( ExecutionContext context,
- Path nodePath,
- Map properties ) {
- PathNode targetNode = getNode(nodePath);
- if (targetNode == null) return null;
- if (source.getCustomPropertiesFactory() == null) return targetNode;
-
- Property primaryTypeProp = targetNode.getProperty(JcrLexicon.PRIMARY_TYPE);
- Name primaryTypeName = (Name)primaryTypeProp.getFirstValue();
-
- CustomPropertiesFactory customPropertiesFactory = source.customPropertiesFactory();
- Location location = Location.create(nodePath, targetNode.getUuid());
-
- /*
- * You can't remove any of the protected properties that the repository provides by default, but you could
- * remove custom properties.
- */
- if (JcrNtLexicon.FILE.equals(primaryTypeName)) {
- customPropertiesFactory.recordFileProperties(context, getSourceName(), location, fileFor(nodePath), properties);
- } else if (ModeShapeLexicon.RESOURCE.equals(primaryTypeName)) {
- File file = fileFor(nodePath.getParent());
- customPropertiesFactory.recordResourceProperties(context, getSourceName(), location, file, properties);
- } else {
- File file = fileFor(nodePath);
- customPropertiesFactory.recordDirectoryProperties(context, getSourceName(), location, file, properties);
- }
-
- PathNode node = getNode(nodePath);
- getCache(getName()).set(node);
-
- return node;
- }
-
@Override
- public PathNode moveNode( ExecutionContext context,
- PathNode node,
- Name desiredNewName,
- WritablePathWorkspace originalWorkspace,
- PathNode newParent,
- PathNode beforeNode ) {
- if (beforeNode != null) {
- throw new InvalidRequestException(FileSystemI18n.nodeOrderingNotSupported.text(getSourceName()));
- }
- PathNode movedNode = super.moveNode(context, node, desiredNewName, originalWorkspace, newParent, beforeNode);
-
- getCache(getName()).invalidate(node.getPath());
+ public FileSystemWorkspace getWorkspace( String name,
+ FileSystemWorkspace originalToClone ) throws InvalidWorkspaceException {
+ FileSystemRepository repository = FileSystemRepository.this;
- return movedNode;
- }
-
- public Path getLowestExistingPath( Path path ) {
- File file = workspaceRoot;
- for (Path.Segment segment : path) {
- String localName = segment.getName().getLocalName();
- // Verify the segment is valid ...
- if (segment.getIndex() > 1) {
- break;
- }
-
- String defaultNamespaceUri = context.getNamespaceRegistry().getDefaultNamespaceUri();
- if (!segment.getName().getNamespaceUri().equals(defaultNamespaceUri)) {
- break;
- }
-
- // The segment should exist as a child of the file ...
- file = new File(file, localName);
- if (!file.exists() || !file.canRead()) {
- // Unable to complete the path, so prepare the exception by determining the lowest path that exists ...
- Path lowest = path;
- while (lowest.getLastSegment() != segment) {
- lowest = lowest.getParent();
- }
- return lowest.getParent();
- }
+ if (originalToClone != null) {
+ return new FileSystemWorkspace(name, originalToClone, repository.getWorkspaceDirectory(name));
}
- // Shouldn't be able to get this far is path is truly invalid
- return path;
+ return new FileSystemWorkspace(repository, name);
}
- public PathNode getNode( Path path ) {
- WorkspaceCache cache = getCache(getName());
-
- PathNode node = cache.get(path);
- if (node != null) return node;
-
- Map properties = new HashMap();
-
- PropertyFactory factory = context.getPropertyFactory();
- PathFactory pathFactory = context.getValueFactories().getPathFactory();
- DateTimeFactory dateFactory = context.getValueFactories().getDateFactory();
- MimeTypeDetector mimeTypeDetector = context.getMimeTypeDetector();
- CustomPropertiesFactory customPropertiesFactory = source.customPropertiesFactory();
- NamespaceRegistry registry = context.getNamespaceRegistry();
- Location location = Location.create(path);
-
- if (!path.isRoot() && JcrLexicon.CONTENT.equals(path.getLastSegment().getName())) {
- File file = fileFor(path.getParent());
- if (file == null) return null;
- // Discover the mime type ...
- String mimeType = null;
- InputStream contents = null;
- try {
- contents = new BufferedInputStream(new FileInputStream(file));
- mimeType = mimeTypeDetector.mimeTypeOf(file.getName(), contents);
- if (mimeType == null) mimeType = DEFAULT_MIME_TYPE;
- properties.put(JcrLexicon.MIMETYPE, factory.create(JcrLexicon.MIMETYPE, mimeType));
- } catch (IOException e) {
- I18n msg = FileSystemI18n.couldNotReadData;
- throw new RepositorySourceException(getSourceName(), msg.text(getSourceName(),
- getName(),
- path.getString(registry)));
- } finally {
- if (contents != null) {
- try {
- contents.close();
- } catch (IOException e) {
- }
- }
- }
-
- // First add any custom properties ...
- Collection customProps = customPropertiesFactory.getResourceProperties(context,
- location,
- file,
- mimeType);
- for (Property customProp : customProps) {
- properties.put(customProp.getName(), customProp);
- }
-
- // The request is to get properties of the "jcr:content" child node ...
- // ... use the dna:resource node type. This is the same as nt:resource, but is not referenceable
- // since we cannot assume that we control all access to this file and can track its movements
- properties.put(JcrLexicon.PRIMARY_TYPE, factory.create(JcrLexicon.PRIMARY_TYPE, ModeShapeLexicon.RESOURCE));
- properties.put(JcrLexicon.LAST_MODIFIED, factory.create(JcrLexicon.LAST_MODIFIED,
- dateFactory.create(file.lastModified())));
- // Don't really know the encoding, either ...
- // request.addProperty(factory.create(JcrLexicon.ENCODED, stringFactory.create("UTF-8")));
-
- // Now put the file's content into the "jcr:data" property ...
- BinaryFactory binaryFactory = context.getValueFactories().getBinaryFactory();
- properties.put(JcrLexicon.DATA, factory.create(JcrLexicon.DATA, binaryFactory.create(file)));
- return new DefaultPathNode(path, null, properties, Collections.emptyList());
- }
-
- File file = fileFor(path);
- if (file == null) return null;
-
- if (file.isDirectory()) {
- String[] childNames = file.list(source.filenameFilter());
- Arrays.sort(childNames);
-
- List childSegments = new ArrayList(childNames.length);
- for (String childName : childNames) {
- childSegments.add(pathFactory.createSegment(childName));
- }
-
- Collection customProps = customPropertiesFactory.getDirectoryProperties(context, location, file);
- for (Property customProp : customProps) {
- properties.put(customProp.getName(), customProp);
- }
-
- if (path.isRoot()) {
- properties.put(JcrLexicon.PRIMARY_TYPE, factory.create(JcrLexicon.PRIMARY_TYPE, ModeShapeLexicon.ROOT));
- return new DefaultPathNode(path, source.getRootNodeUuid(), properties, childSegments);
- }
- properties.put(JcrLexicon.PRIMARY_TYPE, factory.create(JcrLexicon.PRIMARY_TYPE, JcrNtLexicon.FOLDER));
- return new DefaultPathNode(path, source.getRootNodeUuid(), properties, childSegments);
- }
-
- Collection customProps = customPropertiesFactory.getFileProperties(context, location, file);
- for (Property customProp : customProps) {
- properties.put(customProp.getName(), customProp);
- }
- properties.put(JcrLexicon.PRIMARY_TYPE, factory.create(JcrLexicon.PRIMARY_TYPE, JcrNtLexicon.FILE));
- properties.put(JcrLexicon.CREATED, factory.create(JcrLexicon.CREATED, dateFactory.create(file.lastModified())));
- node = new DefaultPathNode(path, null, properties,
- Collections.singletonList(pathFactory.createSegment(JcrLexicon.CONTENT)));
-
- cache.set(node);
- return node;
+ @Override
+ protected void validateNode( FileSystemWorkspace workspace,
+ PathNode node ) {
+ workspace.validate(node);
}
+ }
- /**
- * This utility files the existing {@link File} at the supplied path, and in the process will verify that the path is
- * actually valid.
- *
- * Note that this connector represents a file as two nodes: a parent node with a name that matches the file and a "
- * jcr:primaryType
" of "nt:file
"; and a child node with the name "jcr:content
- * " and a " jcr:primaryType
" of "nt:resource
". The parent "nt:file
" node and its
- * properties represents the file itself, whereas the child "nt:resource
" node and its properties represent
- * the content of the file.
- *
- *
- * As such, this method will return the File object for paths representing both the parent "nt:file
- * " and child " nt:resource
" node.
- *
- *
- * @param path
- * @return the existing {@link File file} for the path; or null if the path does not represent an existing file and a
- * {@link PathNotFoundException} was set as the {@link Request#setError(Throwable) error} on the request
- */
- protected File fileFor( Path path ) {
- assert path != null;
- if (path.isRoot()) {
- return workspaceRoot;
- }
- // See if the path is a "jcr:content" node ...
- if (path.getLastSegment().getName().equals(JcrLexicon.CONTENT)) {
- // We only want to use the parent path to find the actual file ...
- path = path.getParent();
- }
- File file = workspaceRoot;
- for (Path.Segment segment : path) {
- String localName = segment.getName().getLocalName();
- // Verify the segment is valid ...
- if (segment.getIndex() > 1) {
- I18n msg = FileSystemI18n.sameNameSiblingsAreNotAllowed;
- throw new RepositorySourceException(getSourceName(), msg.text(getSourceName()));
- }
-
- String defaultNamespaceUri = context.getNamespaceRegistry().getDefaultNamespaceUri();
- if (!segment.getName().getNamespaceUri().equals(defaultNamespaceUri)) {
- I18n msg = FileSystemI18n.onlyTheDefaultNamespaceIsAllowed;
- throw new RepositorySourceException(getSourceName(), msg.text(getSourceName()));
- }
-
- // The segment should exist as a child of the file ...
- file = new File(file, localName);
- if (!file.exists() || !file.canRead()) {
- return null;
- }
- }
- assert file != null;
- return file;
- }
+ /**
+ * Custom {@link Processor} for the file system connector. This processor throws accurate exceptions on attempts to reorder
+ * nodes, since the file system connector does not support node ordering. Otherwise, it provides default behavior.
+ */
+ class FileSystemProcessor extends Processor {
- protected void ensureValidPathLength( File root ) {
- ensureValidPathLength(root, 0);
+ public FileSystemProcessor( Transaction txn,
+ Repository repository,
+ Observer observer,
+ boolean updatesAllowed ) {
+ super(txn, repository, observer, updatesAllowed);
}
- /**
- * Recursively checks if any of the files in the tree rooted at {@code root} would exceed the
- * {@link FileSystemSource#getMaxPathLength() maximum path length for the processor} if their paths were {@code delta}
- * characters longer. If any files would exceed this length, a {@link RepositorySourceException} is thrown.
- *
- * @param root the root of the tree to check; may be a file or directory but may not be null
- * @param delta the change in the length of the path to check. Used to preemptively check whether moving a file or
- * directory to a new path would violate path length rules
- * @throws RepositorySourceException if any files in the tree rooted at {@code root} would exceed this
- * {@link FileSystemSource#getMaxPathLength() the maximum path length for this processor}
- */
- protected void ensureValidPathLength( File root,
- int delta ) {
- try {
- int len = root.getCanonicalPath().length();
- if (len > source.getMaxPathLength() - delta) {
- String msg = FileSystemI18n.maxPathLengthExceeded.text(source.getMaxPathLength(),
- getSourceName(),
- root.getCanonicalPath(),
- delta);
- throw new RepositorySourceException(getSourceName(), msg);
- }
-
- if (root.isDirectory()) {
- for (File child : root.listFiles(source.filenameFilter())) {
- ensureValidPathLength(child, delta);
- }
-
- }
- } catch (IOException ioe) {
- throw new RepositorySourceException(getSourceName(), FileSystemI18n.getCanonicalPathFailed.text(), ioe);
+ @Override
+ public void process( MoveBranchRequest request ) {
+ if (request.before() != null) {
+ I18n msg = FileSystemI18n.nodeOrderingNotSupported;
+ throw new InvalidRequestException(msg.text(source.getName()));
}
+ super.process(request);
}
}
Index: extensions/modeshape-connector-filesystem/src/main/java/org/modeshape/connector/filesystem/FileSystemSource.java
===================================================================
--- extensions/modeshape-connector-filesystem/src/main/java/org/modeshape/connector/filesystem/FileSystemSource.java (revision 1823)
+++ extensions/modeshape-connector-filesystem/src/main/java/org/modeshape/connector/filesystem/FileSystemSource.java (working copy)
@@ -44,6 +44,7 @@ import net.jcip.annotations.ThreadSafe;
import org.modeshape.common.i18n.I18n;
import org.modeshape.common.util.CheckArg;
import org.modeshape.common.util.StringUtil;
+import org.modeshape.connector.filesystem.FileSystemRepository.FileSystemTransaction;
import org.modeshape.graph.ExecutionContext;
import org.modeshape.graph.JcrLexicon;
import org.modeshape.graph.Location;
@@ -52,11 +53,13 @@ import org.modeshape.graph.connector.RepositoryConnection;
import org.modeshape.graph.connector.RepositorySource;
import org.modeshape.graph.connector.RepositorySourceCapabilities;
import org.modeshape.graph.connector.RepositorySourceException;
-import org.modeshape.graph.connector.path.AbstractPathRepositorySource;
-import org.modeshape.graph.connector.path.PathRepositoryConnection;
+import org.modeshape.graph.connector.base.AbstractRepositorySource;
+import org.modeshape.graph.connector.base.Connection;
+import org.modeshape.graph.connector.base.PathNode;
import org.modeshape.graph.property.Name;
import org.modeshape.graph.property.NamespaceRegistry;
import org.modeshape.graph.property.Property;
+import org.modeshape.graph.request.CreateWorkspaceRequest.CreateConflictBehavior;
/**
* The {@link RepositorySource} for the connector that exposes an area of the local file system as content in a repository. This
@@ -64,7 +67,7 @@ import org.modeshape.graph.property.Property;
* workspace. New workspaces can be created, as long as the names represent valid paths to existing directories.
*/
@ThreadSafe
-public class FileSystemSource extends AbstractPathRepositorySource implements ObjectFactory {
+public class FileSystemSource extends AbstractRepositorySource implements ObjectFactory {
/**
* An immutable {@link CustomPropertiesFactory} implementation that is used by default when none is provided. Note that this
@@ -133,6 +136,8 @@ public class FileSystemSource extends AbstractPathRepositorySource implements Ob
private transient FileSystemRepository repository;
private volatile CustomPropertiesFactory customPropertiesFactory;
+ private ExecutionContext defaultContext = new ExecutionContext();
+
/**
*
*/
@@ -527,8 +532,22 @@ public class FileSystemSource extends AbstractPathRepositorySource implements Ob
throw new RepositorySourceException(getName(), msg.text("name"));
}
- if (repository == null) repository = new FileSystemRepository(this);
- return new PathRepositoryConnection(this, repository);
+ if (repository == null) {
+ repository = new FileSystemRepository(this);
+
+ ExecutionContext context = repositoryContext != null ? repositoryContext.getExecutionContext() : defaultContext;
+ FileSystemTransaction txn = repository.startTransaction(context, false);
+ try {
+ // Create the set of initial workspaces ...
+ for (String initialName : getPredefinedWorkspaceNames()) {
+ repository.createWorkspace(txn, initialName, CreateConflictBehavior.DO_NOT_CREATE, null);
+ }
+ } finally {
+ txn.commit();
+ }
+
+ }
+ return new Connection(this, repository);
}
protected static class StandardPropertiesFactory implements CustomPropertiesFactory {
Index: extensions/modeshape-connector-filesystem/src/main/java/org/modeshape/connector/filesystem/FileSystemWorkspace.java
new file mode 100644
===================================================================
--- /dev/null (revision 1823)
+++ extensions/modeshape-connector-filesystem/src/main/java/org/modeshape/connector/filesystem/FileSystemWorkspace.java (working copy)
@@ -0,0 +1,552 @@
+package org.modeshape.connector.filesystem;
+
+import java.io.BufferedInputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import org.modeshape.common.i18n.I18n;
+import org.modeshape.common.util.FileUtil;
+import org.modeshape.common.util.IoUtil;
+import org.modeshape.graph.ExecutionContext;
+import org.modeshape.graph.JcrLexicon;
+import org.modeshape.graph.JcrNtLexicon;
+import org.modeshape.graph.Location;
+import org.modeshape.graph.ModeShapeLexicon;
+import org.modeshape.graph.connector.RepositorySourceException;
+import org.modeshape.graph.connector.base.PathNode;
+import org.modeshape.graph.connector.base.PathWorkspace;
+import org.modeshape.graph.mimetype.MimeTypeDetector;
+import org.modeshape.graph.property.Binary;
+import org.modeshape.graph.property.BinaryFactory;
+import org.modeshape.graph.property.DateTimeFactory;
+import org.modeshape.graph.property.Name;
+import org.modeshape.graph.property.NameFactory;
+import org.modeshape.graph.property.NamespaceRegistry;
+import org.modeshape.graph.property.Path;
+import org.modeshape.graph.property.PathFactory;
+import org.modeshape.graph.property.PathNotFoundException;
+import org.modeshape.graph.property.Property;
+import org.modeshape.graph.property.PropertyFactory;
+import org.modeshape.graph.property.Path.Segment;
+import org.modeshape.graph.request.Request;
+
+/**
+ * Workspace implementation for the file system connector.
+ */
+class FileSystemWorkspace extends PathWorkspace {
+ private static final String DEFAULT_MIME_TYPE = "application/octet";
+ private static final Set VALID_PRIMARY_TYPES = new HashSet(Arrays.asList(new Name[] {JcrNtLexicon.FOLDER,
+ JcrNtLexicon.FILE, JcrNtLexicon.RESOURCE, ModeShapeLexicon.RESOURCE}));
+
+ private final FileSystemSource source;
+ private final FileSystemRepository repository;
+ private final ExecutionContext context;
+ private final File workspaceRoot;
+
+ public FileSystemWorkspace( String name,
+ FileSystemWorkspace originalToClone,
+ File workspaceRoot ) {
+ super(name, originalToClone.getRootNodeUuid());
+
+ this.source = originalToClone.source;
+ this.context = originalToClone.context;
+ this.workspaceRoot = workspaceRoot;
+ this.repository = originalToClone.repository;
+
+ cloneWorkspace(originalToClone);
+ }
+
+ public FileSystemWorkspace( FileSystemRepository repository,
+ String name ) {
+ super(name, repository.getRootNodeUuid());
+ this.workspaceRoot = repository.getWorkspaceDirectory(name);
+ this.repository = repository;
+ this.context = repository.getContext();
+ this.source = repository.source;
+ }
+
+ private void cloneWorkspace( FileSystemWorkspace original ) {
+ File originalRoot = repository.getWorkspaceDirectory(original.getName());
+ File newRoot = repository.getWorkspaceDirectory(this.getName());
+
+ try {
+ FileUtil.copy(originalRoot, newRoot);
+ } catch (IOException ioe) {
+ throw new IllegalStateException(ioe);
+ }
+ }
+
+ @Override
+ public PathNode moveNode( PathNode node,
+ PathNode newNode ) {
+ PathFactory pathFactory = context.getValueFactories().getPathFactory();
+ Path newPath = pathFactory.create(newNode.getParent(), newNode.getName());
+
+ File originalFile = fileFor(pathFactory.create(node.getParent(), node.getName()));
+ File newFile = fileFor(newPath, false);
+
+ if (newFile.exists()) {
+ newFile.delete();
+ }
+
+ originalFile.renameTo(newFile);
+
+ return getNode(newPath);
+ }
+
+ @Override
+ public PathNode putNode( PathNode node ) {
+ NameFactory nameFactory = context.getValueFactories().getNameFactory();
+ PathFactory pathFactory = context.getValueFactories().getPathFactory();
+ NamespaceRegistry registry = context.getNamespaceRegistry();
+ CustomPropertiesFactory customPropertiesFactory = source.customPropertiesFactory();
+
+ Map properties = node.getProperties();
+
+ if (node.getParent() == null) {
+ // Root node
+ Path rootPath = pathFactory.createRootPath();
+ Location rootLocation = Location.create(rootPath, repository.getRootNodeUuid());
+ customPropertiesFactory.recordDirectoryProperties(context,
+ source.getName(),
+ rootLocation,
+ workspaceRoot,
+ node.getProperties());
+ return getNode(rootPath);
+ }
+
+ /*
+ * Get references to java.io.Files
+ */
+ Path parentPath = node.getParent();
+ boolean isRoot = parentPath == null;
+ File parentFile = fileFor(parentPath);
+
+ Path newPath = isRoot ? pathFactory.createRootPath() : pathFactory.create(parentPath, node.getName());
+ Name name = node.getName().getName();
+ String newName = name.getString(registry);
+ File newFile = new File(parentFile, newName);
+
+ /*
+ * Determine the node primary type
+ */
+ Property primaryTypeProp = properties.get(JcrLexicon.PRIMARY_TYPE);
+
+ // Default primary type to nt:folder
+ Name primaryType = primaryTypeProp == null ? JcrNtLexicon.FOLDER : nameFactory.create(primaryTypeProp.getFirstValue());
+
+ if (JcrNtLexicon.FILE.equals(primaryType)) {
+
+ // The FILE node is represented by the existence of the file
+ if (!parentFile.canWrite()) {
+ I18n msg = FileSystemI18n.parentIsReadOnly;
+ throw new RepositorySourceException(source.getName(), msg.text(parentPath, this.getName(), source.getName()));
+ }
+
+ try {
+ ensureValidPathLength(newFile);
+
+ // Don't try to write if the node conflict behavior is DO_NOT_REPLACE
+ if (!newFile.createNewFile()) {
+ I18n msg = FileSystemI18n.fileAlreadyExists;
+ throw new RepositorySourceException(source.getName(), msg.text(parentPath, getName(), source.getName()));
+ }
+ } catch (IOException ioe) {
+ I18n msg = FileSystemI18n.couldNotCreateFile;
+ throw new RepositorySourceException(source.getName(), msg.text(parentPath,
+ getName(),
+ source.getName(),
+ ioe.getMessage()), ioe);
+ }
+
+ customPropertiesFactory.recordFileProperties(context, source.getName(), Location.create(newPath), newFile, properties);
+ } else if (JcrNtLexicon.RESOURCE.equals(primaryType) || ModeShapeLexicon.RESOURCE.equals(primaryType)) {
+ assert parentFile != null;
+
+ if (!JcrLexicon.CONTENT.equals(name)) {
+ I18n msg = FileSystemI18n.invalidNameForResource;
+ String nodeName = name.getString();
+ throw new RepositorySourceException(source.getName(), msg.text(parentPath, getName(), source.getName(), nodeName));
+ }
+
+ if (!parentFile.isFile()) {
+ I18n msg = FileSystemI18n.invalidPathForResource;
+ throw new RepositorySourceException(source.getName(), msg.text(parentPath, getName(), source.getName()));
+ }
+
+ if (!parentFile.canWrite()) {
+ I18n msg = FileSystemI18n.parentIsReadOnly;
+ throw new RepositorySourceException(source.getName(), msg.text(parentPath, getName(), source.getName()));
+ }
+
+ // Copy over data into a temp file, then move it to the correct location
+ FileOutputStream fos = null;
+ try {
+ File temp = File.createTempFile("dna", null);
+ fos = new FileOutputStream(temp);
+
+ Property dataProp = properties.get(JcrLexicon.DATA);
+ if (dataProp == null) {
+ I18n msg = FileSystemI18n.missingRequiredProperty;
+ String dataPropName = JcrLexicon.DATA.getString();
+ throw new RepositorySourceException(source.getName(), msg.text(parentPath,
+ getName(),
+ source.getName(),
+ dataPropName));
+ }
+
+ BinaryFactory binaryFactory = context.getValueFactories().getBinaryFactory();
+ Binary binary = binaryFactory.create(properties.get(JcrLexicon.DATA).getFirstValue());
+
+ IoUtil.write(binary.getStream(), fos);
+
+ if (!FileUtil.delete(parentFile)) {
+ I18n msg = FileSystemI18n.deleteFailed;
+ throw new RepositorySourceException(source.getName(), msg.text(parentPath, getName(), source.getName()));
+ }
+
+ if (!temp.renameTo(parentFile)) {
+ I18n msg = FileSystemI18n.couldNotUpdateData;
+ throw new RepositorySourceException(source.getName(), msg.text(parentPath, getName(), source.getName()));
+ }
+ } catch (IOException ioe) {
+ I18n msg = FileSystemI18n.couldNotWriteData;
+ throw new RepositorySourceException(source.getName(), msg.text(parentPath,
+ getName(),
+ source.getName(),
+ ioe.getMessage()), ioe);
+
+ } finally {
+ try {
+ if (fos != null) fos.close();
+ } catch (Exception ex) {
+ }
+ }
+ customPropertiesFactory.recordResourceProperties(context,
+ source.getName(),
+ Location.create(parentPath),
+ newFile,
+ properties);
+
+ } else if (JcrNtLexicon.FOLDER.equals(primaryType) || primaryType == null) {
+ ensureValidPathLength(newFile);
+
+ if (!newFile.exists() && !newFile.mkdir()) {
+ I18n msg = FileSystemI18n.couldNotCreateFile;
+ throw new RepositorySourceException(source.getName(),
+ msg.text(parentPath,
+ getName(),
+ source.getName(),
+ primaryType == null ? "null" : primaryType.getString(registry)));
+ }
+ customPropertiesFactory.recordDirectoryProperties(context,
+ source.getName(),
+ Location.create(newPath),
+ newFile,
+ properties);
+
+ } else {
+ // Set error and return
+ I18n msg = FileSystemI18n.unsupportedPrimaryType;
+ throw new RepositorySourceException(source.getName(), msg.text(primaryType.getString(registry),
+ parentPath,
+ getName(),
+ source.getName()));
+ }
+
+ node = getNode(newPath);
+
+ return node;
+ }
+
+ @Override
+ public PathNode removeNode( Path nodePath ) {
+ File nodeFile;
+
+ if (!nodePath.isRoot() && JcrLexicon.CONTENT.equals(nodePath.getLastSegment().getName())) {
+ nodeFile = fileFor(nodePath.getParent());
+ if (!nodeFile.exists()) return null;
+
+ FileOutputStream fos = null;
+ try {
+ fos = new FileOutputStream(nodeFile);
+ IoUtil.write("", fos);
+ } catch (IOException ioe) {
+ throw new RepositorySourceException(source.getName(), FileSystemI18n.deleteFailed.text(nodePath,
+ getName(),
+ source.getName()));
+ } finally {
+ if (fos != null) try {
+ fos.close();
+ } catch (IOException ioe) {
+ }
+ }
+ } else {
+ nodeFile = fileFor(nodePath);
+ if (!nodeFile.exists()) return null;
+
+ FileUtil.delete(nodeFile);
+ }
+
+ return null;
+ }
+
+ @Override
+ public PathNode getRootNode() {
+ return getNode(context.getValueFactories().getPathFactory().createRootPath());
+ }
+
+ @Override
+ public PathNode getNode( Path path ) {
+ Map properties = new HashMap();
+
+ PropertyFactory factory = context.getPropertyFactory();
+ PathFactory pathFactory = context.getValueFactories().getPathFactory();
+ DateTimeFactory dateFactory = context.getValueFactories().getDateFactory();
+ MimeTypeDetector mimeTypeDetector = context.getMimeTypeDetector();
+ CustomPropertiesFactory customPropertiesFactory = source.customPropertiesFactory();
+ NamespaceRegistry registry = context.getNamespaceRegistry();
+ Location location = Location.create(path);
+
+ if (!path.isRoot() && JcrLexicon.CONTENT.equals(path.getLastSegment().getName())) {
+ File file = fileFor(path.getParent());
+ if (file == null) return null;
+ // Discover the mime type ...
+ String mimeType = null;
+ InputStream contents = null;
+ try {
+ contents = new BufferedInputStream(new FileInputStream(file));
+ mimeType = mimeTypeDetector.mimeTypeOf(file.getName(), contents);
+ if (mimeType == null) mimeType = DEFAULT_MIME_TYPE;
+ properties.put(JcrLexicon.MIMETYPE, factory.create(JcrLexicon.MIMETYPE, mimeType));
+ } catch (IOException e) {
+ I18n msg = FileSystemI18n.couldNotReadData;
+ throw new RepositorySourceException(source.getName(), msg.text(source.getName(),
+ getName(),
+ path.getString(registry)));
+ } finally {
+ if (contents != null) {
+ try {
+ contents.close();
+ } catch (IOException e) {
+ }
+ }
+ }
+
+ // First add any custom properties ...
+ Collection customProps = customPropertiesFactory.getResourceProperties(context, location, file, mimeType);
+ for (Property customProp : customProps) {
+ properties.put(customProp.getName(), customProp);
+ }
+
+ // The request is to get properties of the "jcr:content" child node ...
+ // ... use the dna:resource node type. This is the same as nt:resource, but is not referenceable
+ // since we cannot assume that we control all access to this file and can track its movements
+ properties.put(JcrLexicon.PRIMARY_TYPE, factory.create(JcrLexicon.PRIMARY_TYPE, ModeShapeLexicon.RESOURCE));
+ properties.put(JcrLexicon.LAST_MODIFIED, factory.create(JcrLexicon.LAST_MODIFIED,
+ dateFactory.create(file.lastModified())));
+ // Don't really know the encoding, either ...
+ // request.addProperty(factory.create(JcrLexicon.ENCODED, stringFactory.create("UTF-8")));
+
+ // Now put the file's content into the "jcr:data" property ...
+ BinaryFactory binaryFactory = context.getValueFactories().getBinaryFactory();
+ properties.put(JcrLexicon.DATA, factory.create(JcrLexicon.DATA, binaryFactory.create(file)));
+ // return new PathNode(path, null, properties, Collections.emptyList());
+ return new PathNode(null, path.getParent(), path.getLastSegment(), properties, Collections.emptyList());
+ }
+
+ File file = fileFor(path);
+ if (file == null) return null;
+
+ if (file.isDirectory()) {
+ String[] childNames = file.list(source.filenameFilter());
+ Arrays.sort(childNames);
+
+ List childSegments = new ArrayList(childNames.length);
+ for (String childName : childNames) {
+ childSegments.add(pathFactory.createSegment(childName));
+ }
+
+ Collection customProps = customPropertiesFactory.getDirectoryProperties(context, location, file);
+ for (Property customProp : customProps) {
+ properties.put(customProp.getName(), customProp);
+ }
+
+ if (path.isRoot()) {
+ properties.put(JcrLexicon.PRIMARY_TYPE, factory.create(JcrLexicon.PRIMARY_TYPE, ModeShapeLexicon.ROOT));
+ // return new DefaultPathNode(path, source.getRootNodeUuidObject(), properties, childSegments);
+ return new PathNode(source.getRootNodeUuidObject(), path.getParent(), path.getLastSegment(), properties,
+ childSegments);
+
+ }
+ properties.put(JcrLexicon.PRIMARY_TYPE, factory.create(JcrLexicon.PRIMARY_TYPE, JcrNtLexicon.FOLDER));
+ // return new DefaultPathNode(path, source.getRootNodeUuidObject(), properties, childSegments);
+ return new PathNode(null, path.getParent(), path.getLastSegment(), properties, childSegments);
+
+ }
+
+ Collection customProps = customPropertiesFactory.getFileProperties(context, location, file);
+ for (Property customProp : customProps) {
+ properties.put(customProp.getName(), customProp);
+ }
+ properties.put(JcrLexicon.PRIMARY_TYPE, factory.create(JcrLexicon.PRIMARY_TYPE, JcrNtLexicon.FILE));
+ properties.put(JcrLexicon.CREATED, factory.create(JcrLexicon.CREATED, dateFactory.create(file.lastModified())));
+ // node = new DefaultPathNode(path, null, properties,
+ // Collections.singletonList(pathFactory.createSegment(JcrLexicon.CONTENT)));
+ return new PathNode(null, path.getParent(), path.getLastSegment(), properties,
+ Collections.singletonList(pathFactory.createSegment(JcrLexicon.CONTENT)));
+ }
+ /**
+ * This utility files the existing {@link File} at the supplied path, and in the process will verify that the path is actually
+ * valid.
+ *
+ * Note that this connector represents a file as two nodes: a parent node with a name that matches the file and a "
+ * jcr:primaryType
" of "nt:file
"; and a child node with the name "jcr:content
" and a "
+ * jcr:primaryType
" of "nt:resource
". The parent "nt:file
" node and its properties
+ * represents the file itself, whereas the child "nt:resource
" node and its properties represent the content of
+ * the file.
+ *
+ *
+ * As such, this method will return the File object for paths representing both the parent "nt:file
" and child "
+ * nt:resource
" node.
+ *
+ *
+ * @param path
+ * @return the existing {@link File file} for the path; or null if the path does not represent an existing file and a
+ * {@link PathNotFoundException} was set as the {@link Request#setError(Throwable) error} on the request
+ */
+ protected File fileFor( Path path ) {
+ return fileFor(path, true);
+ }
+
+ /**
+ * This utility files the existing {@link File} at the supplied path, and in the process will verify that the path is actually
+ * valid.
+ *
+ * Note that this connector represents a file as two nodes: a parent node with a name that matches the file and a "
+ * jcr:primaryType
" of "nt:file
"; and a child node with the name "jcr:content
" and a "
+ * jcr:primaryType
" of "nt:resource
". The parent "nt:file
" node and its properties
+ * represents the file itself, whereas the child "nt:resource
" node and its properties represent the content of
+ * the file.
+ *
+ *
+ * As such, this method will return the File object for paths representing both the parent "nt:file
" and child "
+ * nt:resource
" node.
+ *
+ *
+ * @param path
+ * @param existingFilesOnly
+ * @return the existing {@link File file} for the path; or null if the path does not represent an existing file and a
+ * {@link PathNotFoundException} was set as the {@link Request#setError(Throwable) error} on the request
+ */
+ protected File fileFor( Path path,
+ boolean existingFilesOnly ) {
+ if (path == null || path.isRoot()) {
+ return workspaceRoot;
+ }
+ // See if the path is a "jcr:content" node ...
+ if (path.getLastSegment().getName().equals(JcrLexicon.CONTENT)) {
+ // We only want to use the parent path to find the actual file ...
+ path = path.getParent();
+ }
+ File file = workspaceRoot;
+ for (Path.Segment segment : path) {
+ String localName = segment.getName().getLocalName();
+ // Verify the segment is valid ...
+ if (segment.getIndex() > 1) {
+ I18n msg = FileSystemI18n.sameNameSiblingsAreNotAllowed;
+ throw new RepositorySourceException(source.getName(), msg.text(source.getName()));
+ }
+
+ String defaultNamespaceUri = context.getNamespaceRegistry().getDefaultNamespaceUri();
+ if (!segment.getName().getNamespaceUri().equals(defaultNamespaceUri)) {
+ I18n msg = FileSystemI18n.onlyTheDefaultNamespaceIsAllowed;
+ throw new RepositorySourceException(source.getName(), msg.text(source.getName()));
+ }
+
+ // The segment should exist as a child of the file ...
+ file = new File(file, localName);
+
+ if (existingFilesOnly && (!file.canRead() || !file.exists())) {
+ return null;
+ }
+ }
+ assert file != null;
+ return file;
+ }
+
+ protected void validate( PathNode node ) {
+ // Don't validate the root node
+ if (node.getParent() == null) return;
+
+ NameFactory nameFactory = context.getValueFactories().getNameFactory();
+ Map properties = node.getProperties();
+ Property primaryTypeProp = properties.get(JcrLexicon.PRIMARY_TYPE);
+ Name primaryType = primaryTypeProp == null ? JcrNtLexicon.FOLDER : nameFactory.create(primaryTypeProp.getFirstValue());
+
+ if (!VALID_PRIMARY_TYPES.contains(primaryType)) {
+ // Set error and return
+ I18n msg = FileSystemI18n.unsupportedPrimaryType;
+ NamespaceRegistry registry = context.getNamespaceRegistry();
+ Path parentPath = node.getParent();
+ throw new RepositorySourceException(source.getName(), msg.text(primaryType.getString(registry),
+ parentPath,
+ getName(),
+ source.getName()));
+
+ }
+
+ Path nodePath = context.getValueFactories().getPathFactory().create(node.getParent(), node.getName());
+ ensureValidPathLength(fileFor(nodePath, false));
+ }
+
+ protected void ensureValidPathLength( File file ) {
+ ensureValidPathLength(file, 0);
+ }
+
+ /**
+ * Recursively checks if any of the files in the tree rooted at {@code root} would exceed the
+ * {@link FileSystemSource#getMaxPathLength() maximum path length for the processor} if their paths were {@code delta}
+ * characters longer. If any files would exceed this length, a {@link RepositorySourceException} is thrown.
+ *
+ * @param root the root of the tree to check; may be a file or directory but may not be null
+ * @param delta the change in the length of the path to check. Used to preemptively check whether moving a file or directory
+ * to a new path would violate path length rules
+ * @throws RepositorySourceException if any files in the tree rooted at {@code root} would exceed this
+ * {@link FileSystemSource#getMaxPathLength() the maximum path length for this processor}
+ */
+ protected void ensureValidPathLength( File root,
+ int delta ) {
+ try {
+ int len = root.getCanonicalPath().length();
+ if (len > source.getMaxPathLength() - delta) {
+ String msg = FileSystemI18n.maxPathLengthExceeded.text(source.getMaxPathLength(),
+ source.getName(),
+ root.getCanonicalPath(),
+ delta);
+ throw new RepositorySourceException(source.getName(), msg);
+ }
+
+ if (root.isDirectory()) {
+ for (File child : root.listFiles(source.filenameFilter())) {
+ ensureValidPathLength(child, delta);
+ }
+
+ }
+ } catch (IOException ioe) {
+ throw new RepositorySourceException(source.getName(), FileSystemI18n.getCanonicalPathFailed.text(), ioe);
+ }
+ }
+
+}
Index: extensions/modeshape-connector-filesystem/src/main/java/org/modeshape/connector/filesystem/package-info.java
===================================================================
--- extensions/modeshape-connector-filesystem/src/main/java/org/modeshape/connector/filesystem/package-info.java (revision 1823)
+++ extensions/modeshape-connector-filesystem/src/main/java/org/modeshape/connector/filesystem/package-info.java (working copy)
@@ -23,7 +23,9 @@
*/
/**
* The classes that make up the connector that accesses the files and directories on a local file system and exposes them as content in a repository.
- * This connector is based on the {@link org.modeshape.graph.connector.path.WritablePathRepository path repository framework}.
+ * This connector is based on the {@link org.modeshape.graph.connector.base.PathWorkspace path repository framework}.
+ * @see org.modeshape.graph.connector.base.PathWorkspace
+ * @see org.modeshape.graph.connector.base.PathTransaction
*/
package org.modeshape.connector.filesystem;
Index: extensions/modeshape-connector-filesystem/src/main/resources/org/modeshape/connector/filesystem/FileSystemI18n.properties
===================================================================
--- extensions/modeshape-connector-filesystem/src/main/resources/org/modeshape/connector/filesystem/FileSystemI18n.properties (revision 1823)
+++ extensions/modeshape-connector-filesystem/src/main/resources/org/modeshape/connector/filesystem/FileSystemI18n.properties (working copy)
@@ -27,8 +27,7 @@ pathForWorkspaceRootDoesNotExist = The path "{0}" for the predefined workspace f
pathForWorkspaceRootIsNotDirectory = The path "{0}" for the predefined workspace for the file system source "{1}" is actually a path to an existing file
pathForWorkspaceRootCannotBeRead = The path "{0}" for the predefined workspace for the file system source "{1}" cannot be read
propertyIsRequired = The {0} property is required but has no value
-locationInRequestMustHavePath = {0} requires a path in the request: {1}
-sameNameSiblingsAreNotAllowed = Repository source "{0}" does not allow same name siblings on nodes: {1}
+sameNameSiblingsAreNotAllowed = The "{0}" source does not allow same-name siblings
nodeOrderingNotSupported = {0} does not support node ordering
onlyTheDefaultNamespaceIsAllowed = Repository source "{0}" requires that node names use the default namespace
sourceIsReadOnly = The source "{0}" does not allow updates. Set the "updatesAllowed" property to "true" on the repository source (connector) to enable updates.
@@ -52,6 +51,5 @@ couldNotWriteData = Error writing data to path "{0}" in workspace "{1}" in {2}\:
couldNotUpdateData = Error moving temporary data file to path "{0}" in workspace "{1}" in {2}
missingRequiredProperty = Missing required property "{3}" at path "{0}" in workspace "{1}" in {2}
deleteFailed = Could not delete file at path "{0}" in workspace "{1}" in {2}
-copyFailed = Could not copy file at path "{0}" in workspace "{1}" to path "{2}" in workspace "{3}" in {4}
getCanonicalPathFailed = Could not determine canonical path
maxPathLengthExceeded = The maximum absolute path length ({0}) for source "{1}" was exceeded by the node at: {2} ({3})
Index: extensions/modeshape-connector-filesystem/src/test/java/org/modeshape/connector/filesystem/FileSystemConnectorCreateWorkspacesTest.java
===================================================================
--- extensions/modeshape-connector-filesystem/src/test/java/org/modeshape/connector/filesystem/FileSystemConnectorCreateWorkspacesTest.java (revision 1823)
+++ extensions/modeshape-connector-filesystem/src/test/java/org/modeshape/connector/filesystem/FileSystemConnectorCreateWorkspacesTest.java (working copy)
@@ -27,11 +27,11 @@ import static org.hamcrest.core.Is.is;
import static org.junit.Assert.assertThat;
import java.util.HashSet;
import java.util.Set;
+import org.junit.Test;
import org.modeshape.graph.Graph;
import org.modeshape.graph.Workspace;
import org.modeshape.graph.connector.RepositorySource;
import org.modeshape.graph.connector.test.WorkspaceConnectorTest;
-import org.junit.Test;
/**
* These tests verify that the file system connector behaves correctly when the source is configured to
@@ -56,6 +56,7 @@ public class FileSystemConnectorCreateWorkspacesTest extends WorkspaceConnectorT
source.setPredefinedWorkspaceNames(predefinedWorkspaceNames);
source.setDefaultWorkspaceName(predefinedWorkspaceNames[0]);
source.setCreatingWorkspacesAllowed(true);
+ source.setUpdatesAllowed(true);
return source;
}
Index: extensions/modeshape-connector-filesystem/src/test/java/org/modeshape/connector/filesystem/FileSystemConnectorWritableTest.java
===================================================================
--- extensions/modeshape-connector-filesystem/src/test/java/org/modeshape/connector/filesystem/FileSystemConnectorWritableTest.java (revision 1823)
+++ extensions/modeshape-connector-filesystem/src/test/java/org/modeshape/connector/filesystem/FileSystemConnectorWritableTest.java (working copy)
@@ -24,6 +24,7 @@
package org.modeshape.connector.filesystem;
import static org.hamcrest.core.Is.is;
+import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
@@ -32,11 +33,12 @@ import java.io.FileInputStream;
import java.io.IOException;
import org.junit.Test;
import org.modeshape.common.util.FileUtil;
+import org.modeshape.common.util.StringUtil;
import org.modeshape.graph.Graph;
import org.modeshape.graph.JcrLexicon;
-import org.modeshape.graph.JcrMixLexicon;
import org.modeshape.graph.JcrNtLexicon;
import org.modeshape.graph.ModeShapeLexicon;
+import org.modeshape.graph.Graph.Batch;
import org.modeshape.graph.connector.RepositorySource;
import org.modeshape.graph.connector.RepositorySourceException;
import org.modeshape.graph.connector.test.AbstractConnectorTest;
@@ -185,11 +187,6 @@ public class FileSystemConnectorWritableTest extends AbstractConnectorTest {
graph.create("/testFile").with(JcrLexicon.PRIMARY_TYPE, JcrNtLexicon.UNSTRUCTURED).orReplace().and();
}
- @Test( expected = RepositorySourceException.class )
- public void shouldNotBeAbleToSetArbitraryProperties() {
- graph.create("/testFile").with(JcrLexicon.MIXIN_TYPES, JcrMixLexicon.LOCKABLE).orReplace().and();
- }
-
@Test
public void shouldBeAbleToCopyFile() {
graph.create("/testFile").with(JcrLexicon.PRIMARY_TYPE, JcrNtLexicon.FILE).orReplace().and();
@@ -515,6 +512,32 @@ public class FileSystemConnectorWritableTest extends AbstractConnectorTest {
}
}
+ @Test
+ public void shouldBeAllOrNothing() {
+ String longTestFileName = "/testFileWithTooLongName" + StringUtil.createString('x', 300);
+
+ Batch batch = graph.batch();
+
+ batch.create("/testFile").with(JcrLexicon.PRIMARY_TYPE, JcrNtLexicon.FILE).orReplace().and();
+ batch.create("/testFile/jcr:content")
+ .with(JcrLexicon.PRIMARY_TYPE, ModeShapeLexicon.RESOURCE)
+ .and(JcrLexicon.DATA, TEST_CONTENT.getBytes())
+ .orReplace()
+ .and();
+ batch.create(longTestFileName).and();
+
+ try {
+ batch.execute();
+ fail("The overly long test file name (" + longTestFileName + ") did not fail");
+ } catch (RepositorySourceException rse) {
+ // Expected
+ }
+
+ File newFile = new File(testWorkspaceRoot, "testFile");
+ assertFalse(newFile.exists());
+ }
+
+
protected void assertContents( File file,
String contents ) {
assertTrue(file.exists());
Index: extensions/modeshape-connector-filesystem/testFile
new file mode 100644
===================================================================
--- /dev/null (revision 1823)
+++ extensions/modeshape-connector-filesystem/testFile (working copy)
@@ -0,0 +1 @@
+Test content
\ No newline at end of file
Index: extensions/modeshape-connector-filesystem/testFolder/testFile
new file mode 100644
===================================================================
--- /dev/null (revision 1823)
+++ extensions/modeshape-connector-filesystem/testFolder/testFile (working copy)
@@ -0,0 +1 @@
+Test content
\ No newline at end of file
Index: modeshape-graph/src/main/java/org/modeshape/graph/connector/base/AbstractRepositorySource.java
new file mode 100644
===================================================================
--- /dev/null (revision 1823)
+++ modeshape-graph/src/main/java/org/modeshape/graph/connector/base/AbstractRepositorySource.java (working copy)
@@ -0,0 +1,197 @@
+package org.modeshape.graph.connector.base;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.UUID;
+import javax.naming.BinaryRefAddr;
+import javax.naming.RefAddr;
+import javax.naming.Reference;
+import javax.naming.StringRefAddr;
+import org.modeshape.common.util.CheckArg;
+import org.modeshape.graph.cache.CachePolicy;
+import org.modeshape.graph.connector.RepositoryContext;
+import org.modeshape.graph.connector.RepositorySourceException;
+
+/**
+ * Basic implementation of {@link BaseRepositorySource}, providing default implementations of the accessors and mutators in that
+ * interface.
+ */
+@SuppressWarnings( "serial" )
+public abstract class AbstractRepositorySource implements BaseRepositorySource {
+
+ /**
+ * The default UUID that is used for root nodes in a store.
+ */
+ public static final String DEFAULT_ROOT_NODE_UUID = "cafebabe-cafe-babe-cafe-babecafebabe";
+
+ /**
+ * The default number of times that a request that failed due to system error should be retried
+ */
+ public static final int DEFAULT_RETRY_LIMIT = 0;
+
+ /**
+ * The default cache policy for this repository source (no caching)
+ */
+ public static final CachePolicy DEFAULT_CACHE_POLICY = null;
+
+ protected int retryLimit = DEFAULT_RETRY_LIMIT;
+ protected String name;
+
+ protected transient RepositoryContext repositoryContext;
+ protected transient UUID rootNodeUuid = UUID.fromString(DEFAULT_ROOT_NODE_UUID);
+ protected transient CachePolicy cachePolicy = DEFAULT_CACHE_POLICY;
+
+ /**
+ * {@inheritDoc}
+ *
+ * @see org.modeshape.graph.connector.base.BaseRepositorySource#areUpdatesAllowed()
+ */
+ public boolean areUpdatesAllowed() {
+ return false;
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @see org.modeshape.graph.connector.base.BaseRepositorySource#getRepositoryContext()
+ */
+ public RepositoryContext getRepositoryContext() {
+ return repositoryContext;
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @see org.modeshape.graph.connector.RepositorySource#initialize(RepositoryContext)
+ */
+ public void initialize( RepositoryContext context ) throws RepositorySourceException {
+ CheckArg.isNotNull(context, "context");
+ this.repositoryContext = context;
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @see org.modeshape.graph.connector.base.BaseRepositorySource#getDefaultCachePolicy()
+ */
+ public CachePolicy getDefaultCachePolicy() {
+ return this.cachePolicy;
+ }
+
+ /**
+ * Sets the cache policy for the repository and replaces the path repository cache with a new path repository cache tied to
+ * the new cache policy
+ *
+ * @param cachePolicy the new cache policy; may not be null
+ */
+ public void setCachePolicy( CachePolicy cachePolicy ) {
+ CheckArg.isNotNull(cachePolicy, "cachePolicy");
+ this.cachePolicy = cachePolicy;
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @see org.modeshape.graph.connector.base.BaseRepositorySource#getRootNodeUuidObject()
+ */
+ public UUID getRootNodeUuidObject() {
+ return rootNodeUuid;
+ }
+
+ /**
+ * @param rootNodeUuid Sets rootNodeUuid to the specified value.
+ * @throws IllegalArgumentException if the string value cannot be converted to UUID
+ */
+ public void setRootNodeUuidObject( String rootNodeUuid ) {
+ if (rootNodeUuid != null && rootNodeUuid.trim().length() == 0) rootNodeUuid = DEFAULT_ROOT_NODE_UUID;
+ this.rootNodeUuid = UUID.fromString(rootNodeUuid);
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @see org.modeshape.graph.connector.RepositorySource#close()
+ */
+ public void close() {
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @see org.modeshape.graph.connector.RepositorySource#getName()
+ */
+ public String getName() {
+ return name;
+ }
+
+ /**
+ * Sets the name of the repository source. The name should be unique among loaded repository sources.
+ *
+ * @param name the new name for the repository source; may not be empty
+ */
+ public void setName( String name ) {
+ if (name != null) {
+ name = name.trim();
+ if (name.length() == 0) name = null;
+ }
+
+ this.name = name;
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @see org.modeshape.graph.connector.RepositorySource#getRetryLimit()
+ */
+ public int getRetryLimit() {
+ return retryLimit;
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @see org.modeshape.graph.connector.RepositorySource#setRetryLimit(int limit)
+ */
+ public void setRetryLimit( int limit ) {
+ this.retryLimit = limit < 0 ? 0 : limit;
+ }
+
+ /**
+ * Extracts the values from the given reference, automatically translating {@link BinaryRefAddr} instances into the
+ * deserialized classes that they represent.
+ *
+ * @param ref the reference from which the values should be extracted
+ * @return a map of value names to values from the reference
+ * @throws IOException if there is an error deserializing a {@code BinaryRefAddr}
+ * @throws ClassNotFoundException if a serialized class cannot be deserialized because its class is not in the class path
+ */
+ protected Map valuesFrom( Reference ref ) throws IOException, ClassNotFoundException {
+ Map values = new HashMap();
+
+ Enumeration> en = ref.getAll();
+ while (en.hasMoreElements()) {
+ RefAddr subref = (RefAddr)en.nextElement();
+ if (subref instanceof StringRefAddr) {
+ String key = subref.getType();
+ Object value = subref.getContent();
+ if (value != null) values.put(key, value.toString());
+ } else if (subref instanceof BinaryRefAddr) {
+ String key = subref.getType();
+ Object value = subref.getContent();
+ if (value instanceof byte[]) {
+ // Deserialize ...
+ ByteArrayInputStream bais = new ByteArrayInputStream((byte[])value);
+ ObjectInputStream ois = new ObjectInputStream(bais);
+ value = ois.readObject();
+ values.put(key, value);
+ }
+ }
+ }
+
+ return values;
+ }
+}
Index: modeshape-graph/src/main/java/org/modeshape/graph/connector/base/BaseTransaction.java
===================================================================
--- modeshape-graph/src/main/java/org/modeshape/graph/connector/base/BaseTransaction.java (revision 1823)
+++ modeshape-graph/src/main/java/org/modeshape/graph/connector/base/BaseTransaction.java (working copy)
@@ -25,6 +25,7 @@ package org.modeshape.graph.connector.base;
import java.util.LinkedList;
import java.util.List;
+import java.util.Set;
import java.util.UUID;
import net.jcip.annotations.NotThreadSafe;
import org.modeshape.graph.ExecutionContext;
@@ -55,7 +56,11 @@ public abstract class BaseTransaction repository;
+
protected BaseTransaction( ExecutionContext context,
+ Repository repository,
UUID rootNodeUuid ) {
this.rootNodeUuid = rootNodeUuid;
this.context = context;
@@ -63,6 +68,7 @@ public abstract class BaseTransaction getRepository() {
+ return repository;
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @see org.modeshape.graph.connector.base.Transaction#getWorkspaceNames()
+ */
+ public Set getWorkspaceNames() {
+ return repository.getWorkspaceNames();
+ }
+
+ /**
* {@inheritDoc}
*
* @see org.modeshape.graph.connector.base.Transaction#getRootNode(org.modeshape.graph.connector.base.Workspace)
Index: modeshape-graph/src/main/java/org/modeshape/graph/connector/base/MapTransaction.java
===================================================================
--- modeshape-graph/src/main/java/org/modeshape/graph/connector/base/MapTransaction.java (revision 1823)
+++ modeshape-graph/src/main/java/org/modeshape/graph/connector/base/MapTransaction.java (working copy)
@@ -64,8 +64,6 @@ import org.modeshape.graph.request.FullTextSearchRequest;
public abstract class MapTransaction>
extends BaseTransaction {
- /** The repository against which this transaction is operating */
- private final Repository repository;
/** The set of changes to the workspaces that have been made by this transaction */
private Map changesByWorkspaceName;
@@ -77,17 +75,7 @@ public abstract class MapTransaction repository,
UUID rootNodeUuid ) {
- super(repository.getContext(), rootNodeUuid);
- this.repository = repository;
- }
-
- /**
- * Obtain the repository object against which this transaction is running.
- *
- * @return the repository object; never null
- */
- protected Repository getRepository() {
- return repository;
+ super(repository.getContext(), repository, rootNodeUuid);
}
/**
@@ -690,7 +678,7 @@ public abstract class MapTransaction
+ * Strictly speaking, this class is not immutable or thread safe. However, the persisted state cannot be changed. Instead, any
+ * changes made to the object are stored in a transient area and are made "persistable"via the {@link #freeze()} method.
+ *
+ *
+ * The {@link PathTransaction} maintains an unfrozen, changed instance within it transactional state, and always puts the
+ * {@link #freeze() frozen}, read-only representation inside the
+ */
+public class PathNode implements Node, Serializable, Cloneable {
+
+ private static final long serialVersionUID = 1L;
+
+ /* These members MUST be treated as "final", even though they cannot be to correctly implement Serializable */
+ private/*final*/UUID uuid;
+ private/*final*/Path parent;
+ private/*final*/Segment name;
+ private/*final*/Map properties;
+ private/*final*/List children;
+ private/*final*/int version = 1;
+
+ /** The changes made to this object, making it unfrozen */
+ protected transient Changes changes;
+
+ /**
+ * Create a new node instance.
+ *
+ * @param uuid the UUID of the node; may be null
+ * @param parent the path to the parent node; may be null only if the name is null
+ * @param name the name of this node, relative to the parent
+ * @param properties the unmodifiable map of properties; may be null or empty
+ * @param children the unmodifiable list of child segments; may be null or empty
+ */
+ public PathNode( UUID uuid,
+ Path parent,
+ Segment name,
+ Map properties,
+ List children ) {
+ this.uuid = uuid;
+ this.parent = parent;
+ this.name = name;
+ this.properties = properties != null ? properties : Collections.emptyMap();
+ this.children = children != null ? children : Collections.emptyList();
+ assert this.properties != null;
+ assert this.children != null;
+ assert this.name != null ? this.parent != null : this.parent == null;
+ }
+
+ /**
+ * Create a new node instance.
+ *
+ * @param uuid the UUID of the node; may be null
+ * @param parent the path to the parent node; may be null only if the name is null
+ * @param name the name of this node, relative to the parent
+ * @param properties the unmodifiable map of properties; may be null or empty
+ * @param children the unmodifiable list of child segments; may be null or empty
+ * @param version the version number
+ */
+ protected PathNode( UUID uuid,
+ Path parent,
+ Segment name,
+ Map properties,
+ List children,
+ int version ) {
+ this.uuid = uuid;
+ this.parent = parent;
+ this.name = name;
+ this.properties = properties != null ? properties : Collections.emptyMap();
+ this.children = children != null ? children : Collections.emptyList();
+ this.version = version;
+ assert this.properties != null;
+ assert this.children != null;
+ assert this.name != null ? this.parent != null : this.parent == null;
+ }
+
+ /**
+ * Create a new node instance.
+ *
+ * @param uuid the UUID of the node; may be null
+ * @param parent the path to the parent node; may be null only if the name is null
+ * @param name the name of this node, relative to the parent
+ * @param properties the properties that are to be copied into the new node; may be null or empty
+ * @param children the unmodifiable list of child segments; may be null or empty
+ */
+ public PathNode( UUID uuid,
+ Path parent,
+ Segment name,
+ Iterable properties,
+ List children ) {
+ this.uuid = uuid;
+ this.parent = parent;
+ this.name = name;
+ if (properties != null) {
+ Map props = new HashMap();
+ for (Property prop : properties) {
+ props.put(prop.getName(), prop);
+ }
+ this.properties = props.isEmpty() ? Collections.emptyMap() : Collections.unmodifiableMap(props);
+ } else {
+ this.properties = Collections.emptyMap();
+ }
+ this.children = children != null ? children : Collections.emptyList();
+ assert this.properties != null;
+ assert this.children != null;
+ assert this.name != null ? this.parent != null : this.parent == null;
+ }
+
+ /**
+ * Create a root node with the supplied UUID.
+ *
+ * @param uuid the UUID of the root node; may not be null
+ */
+ public PathNode( UUID uuid ) {
+ this.uuid = uuid;
+ this.parent = null;
+ this.name = null;
+ this.properties = Collections.emptyMap();
+ this.children = Collections.emptyList();
+ assert this.uuid != null;
+ assert this.properties != null;
+ assert this.children != null;
+ }
+
+ /**
+ * Get the version number of this node.
+ *
+ * @return the version number
+ */
+ public int getVersion() {
+ return version;
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @see org.modeshape.graph.connector.base.Node#getUuid()
+ */
+ public UUID getUuid() {
+ return uuid;
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @see org.modeshape.graph.connector.base.Node#getName()
+ */
+ public Segment getName() {
+ return changes != null ? changes.getName() : name;
+ }
+
+ /**
+ * @return parent
+ */
+ public Path getParent() {
+ return changes != null ? changes.getParent() : parent;
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @see org.modeshape.graph.connector.base.Node#getProperties()
+ */
+ public Map getProperties() {
+ return changes != null ? changes.getProperties(false) : properties;
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @see org.modeshape.graph.connector.base.Node#getProperty(org.modeshape.graph.property.Name)
+ */
+ public Property getProperty( Name name ) {
+ return getProperties().get(name);
+ }
+
+ /**
+ * @return children
+ */
+ public List getChildren() {
+ return changes != null ? changes.getChildren(false) : children;
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + ((children == null) ? 0 : children.hashCode());
+ result = prime * result + ((name == null) ? 0 : name.hashCode());
+ result = prime * result + ((parent == null) ? 0 : parent.hashCode());
+ return result;
+ }
+
+ @Override
+ public boolean equals( Object obj ) {
+ if (this == obj) return true;
+ if (obj == null) return false;
+ if (getClass() != obj.getClass()) return false;
+ PathNode other = (PathNode)obj;
+ if (name == null) {
+ if (other.name != null) return false;
+ } else if (!name.equals(other.name)) return false;
+ if (parent == null) {
+ if (other.parent != null) return false;
+ } else if (!parent.equals(other.parent)) return false;
+ return true;
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @see java.lang.Object#toString()
+ */
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ if (this.parent == null) {
+ sb.append("/");
+ } else {
+ sb.append(this.getParent()).append("/").append(this.getName());
+ }
+ sb.append(" (");
+ sb.append(this.getUuid()).append(")");
+ return sb.toString();
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * This method never clones the {@link #hasChanges() changes}.
+ *
+ *
+ * @see java.lang.Object#clone()
+ */
+ @Override
+ public PathNode clone() {
+ return new PathNode(uuid, parent, name, new HashMap(properties), new ArrayList(children));
+ }
+
+ /**
+ * Determine if this node has any unsaved changes.
+ *
+ * @return true if there are unsaved changes, or false otherwise
+ */
+ protected boolean hasChanges() {
+ return changes != null;
+ }
+
+ /**
+ * Create the {@link Changes} implementation. Subclasses that require a specialized class should overwrite this method. Note
+ * that this method does not modify any internal state; it should just instantiate and return the correct Changes class.
+ *
+ * @return the changes object.
+ */
+ protected Changes newChanges() {
+ return new Changes();
+ }
+
+ /**
+ * Return the frozen node with all internal state reflective of any changes. If this node has no changes, this method simply
+ * returns this same node. Otherwise, this method creates a new node that has no changes and that mirrors this node's current
+ * state, and this new node will have an incremented {@link #getVersion() version} number.
+ *
+ * @return the unfrozen node; never null
+ */
+ public PathNode freeze() {
+ if (!hasChanges()) return this;
+ return new PathNode(uuid, changes.getParent(), changes.getName(), changes.getUnmodifiableProperties(),
+ changes.getUnmodifiableChildren(), version + 1);
+ }
+
+ /**
+ * Create a copy of this node except using the supplied path.
+ *
+ * @param parent sets parent to the specified value.
+ * @return the new path node; never null
+ */
+ public PathNode withParent( Path parent ) {
+ if (changes == null) {
+ PathNode copy = clone();
+ copy.changes = newChanges();
+ copy.changes.setParent(parent);
+ return copy;
+ }
+ changes.setParent(parent);
+ return this;
+ }
+
+ /**
+ * Create a copy of this node except using the supplied name.
+ *
+ * @param name sets name to the specified value.
+ * @return the new path node; never null
+ */
+ public PathNode withName( Segment name ) {
+ if (changes == null) {
+ PathNode copy = clone();
+ copy.changes = newChanges();
+ copy.changes.setName(name);
+ return copy;
+ }
+ changes.setName(name);
+ return this;
+ }
+
+ /**
+ * Create a copy of this node except adding the supplied node at the end of the existing children.
+ *
+ * @param child the segment of the child that is to be added; may not be null
+ * @return the new path node; never null
+ */
+ public PathNode withChild( Segment child ) {
+ assert child != null;
+ if (getChildren().indexOf(child) != -1) return this;
+ if (changes == null) {
+ PathNode copy = clone();
+ List children = new LinkedList(getChildren());
+ assert !children.contains(child);
+ children.add(child);
+ copy.changes = newChanges();
+ copy.changes.setChildren(children);
+ return copy;
+ }
+ changes.getChildren(true).add(child);
+ return this;
+ }
+
+ /**
+ * Create a copy of this node except adding the supplied node into the existing children at the specified index.
+ *
+ * @param index the index at which the child is to appear
+ * @param child the segment of the child that is to be added at the end of the existing children
+ * @return the new path node; never null
+ */
+ public PathNode withChild( int index,
+ Segment child ) {
+ assert child != null;
+ assert index >= 0;
+ int existingIndex = getChildren().indexOf(child);
+ if (existingIndex == index) {
+ // No need to add twice, so simply return (have not yet made any changes)
+ return this;
+ }
+ if (changes == null) {
+ PathNode copy = clone();
+ List children = new LinkedList(getChildren());
+ if (existingIndex >= 0) {
+ // The child is moving positions, so remove it before we add it ...
+ children.remove(existingIndex);
+ if (existingIndex < index) --index;
+ }
+ children.add(index, child);
+ copy.changes = newChanges();
+ copy.changes.setChildren(children);
+ return copy;
+ }
+ List children = changes.getChildren(true);
+ if (existingIndex >= 0) {
+ // The child is moving positions, so remove it before we add it ...
+ children.remove(existingIndex);
+ if (existingIndex < index) --index;
+ }
+ children.add(index, child);
+ return this;
+ }
+
+ /**
+ * Create a copy of this node except without the supplied child node.
+ *
+ * @param child the segment of the child that is to be removed; may not be null
+ * @return the new path node; never null
+ */
+ public PathNode withoutChild( Segment child ) {
+ assert child != null;
+ if (changes == null) {
+ PathNode copy = clone();
+ List children = new LinkedList(getChildren());
+ children.remove(child);
+ copy.changes = newChanges();
+ copy.changes.setChildren(children);
+ return copy;
+ }
+ changes.getChildren(true).remove(child);
+ return this;
+ }
+
+ /**
+ * Create a copy of this node except with none of the children.
+ *
+ * @return the new path node; never null
+ */
+ public PathNode withoutChildren() {
+ if (getChildren().isEmpty()) return this;
+ if (changes == null) {
+ PathNode copy = clone();
+ copy.changes = newChanges();
+ copy.changes.setChildren(new LinkedList());
+ return copy;
+ }
+ changes.getChildren(true).clear();
+ return this;
+ }
+
+ /**
+ * Create a copy of this node except with the changes to the properties.
+ *
+ * @param propertiesToSet the properties that are to be set; may be null if no properties are to be set
+ * @param propertiesToRemove the names of the properties that are to be removed; may be null if no properties are to be
+ * removed
+ * @param removeAllExisting true if all existing properties should be removed
+ * @return the unfrozen path node; never null
+ */
+ public PathNode withProperties( Iterable propertiesToSet,
+ Iterable propertiesToRemove,
+ boolean removeAllExisting ) {
+ if (propertiesToSet == null && propertiesToRemove == null && !removeAllExisting) {
+ // no changes ...
+ return this;
+ }
+ Map newProperties = null;
+ PathNode result = this;
+ if (changes == null) {
+ PathNode copy = clone();
+ copy.changes = newChanges();
+ copy.changes.setProperties(new HashMap(this.properties));
+ newProperties = copy.changes.getProperties(true);
+ result = copy;
+ } else {
+ newProperties = changes.getProperties(true);
+ }
+ if (removeAllExisting) {
+ newProperties.clear();
+ } else {
+ if (propertiesToRemove != null) {
+ for (Name name : propertiesToRemove) {
+ newProperties.remove(name);
+ }
+ } else if (propertiesToSet == null) {
+ return this;
+ }
+ }
+ if (propertiesToSet != null) {
+ for (Property property : propertiesToSet) {
+ newProperties.put(property.getName(), property);
+ }
+ }
+ return result;
+ }
+
+ /**
+ * Create a copy of this node except with the new property.
+ *
+ * @param property the property to set
+ * @return this path node
+ */
+ public PathNode withProperty( Property property ) {
+ if (property == null) return this;
+ if (changes == null) {
+ PathNode copy = clone();
+ copy.changes = newChanges();
+ Map newProps = new HashMap(this.properties);
+ newProps.put(property.getName(), property);
+ copy.changes.setProperties(newProps);
+ return copy;
+ }
+ changes.getProperties(true).put(property.getName(), property);
+ return this;
+ }
+
+ /**
+ * Create a copy of this node except without the new property.
+ *
+ * @param propertyName the name of the property that is to be removed
+ * @return this path node, or this node if the named properties does not exist on this node
+ */
+ public PathNode withoutProperty( Name propertyName ) {
+ if (propertyName == null || !getProperties().containsKey(propertyName)) return this;
+ if (changes == null) {
+ PathNode copy = clone();
+ copy.changes = newChanges();
+ copy.changes.setProperties(new HashMap(this.properties));
+ return copy;
+ }
+ changes.getProperties(true).remove(propertyName);
+ return this;
+ }
+
+ /**
+ * Create a copy of this node without any properties
+ *
+ * @return this path node, or this node if this node has no properties
+ */
+ public PathNode withoutProperties() {
+ if (getProperties().isEmpty()) return this;
+ if (changes == null) {
+ PathNode copy = clone();
+ copy.changes = newChanges();
+ copy.changes.setProperties(new HashMap());
+ return copy;
+ }
+ changes.getProperties(true).clear();
+ return this;
+
+ }
+
+ @SuppressWarnings( "synthetic-access" )
+ protected class Changes {
+ private Path parent;
+ private Segment name;
+ private Map properties;
+ private List children;
+
+ public Path getParent() {
+ return parent != null ? parent : PathNode.this.parent;
+ }
+
+ public void setParent( Path parent ) {
+ this.parent = parent;
+ }
+
+ public Segment getName() {
+ return name != null ? name : PathNode.this.name;
+ }
+
+ public void setName( Segment name ) {
+ this.name = name;
+ }
+
+ public Map getProperties( boolean createIfMissing ) {
+ if (properties == null) {
+ if (createIfMissing) {
+ properties = new HashMap(PathNode.this.properties);
+ return properties;
+ }
+ return PathNode.this.properties;
+ }
+ return properties;
+ }
+
+ public Map getUnmodifiableProperties() {
+ return properties != null ? Collections.unmodifiableMap(properties) : PathNode.this.properties;
+ }
+
+ public void setProperties( Map properties ) {
+ this.properties = properties;
+ }
+
+ public List getChildren( boolean createIfMissing ) {
+ if (children == null) {
+ if (createIfMissing) {
+ children = new LinkedList();
+ return children;
+ }
+ return PathNode.this.children;
+ }
+ return children;
+ }
+
+ public List getUnmodifiableChildren() {
+ return children != null ? Collections.unmodifiableList(children) : PathNode.this.children;
+ }
+
+ public void setChildren( List children ) {
+ this.children = children;
+ }
+ }
+
+}
Index: modeshape-graph/src/main/java/org/modeshape/graph/connector/base/PathTransaction.java
new file mode 100644
===================================================================
--- /dev/null (revision 1823)
+++ modeshape-graph/src/main/java/org/modeshape/graph/connector/base/PathTransaction.java (working copy)
@@ -0,0 +1,808 @@
+/*
+ * ModeShape (http://www.modeshape.org)
+ * See the COPYRIGHT.txt file distributed with this work for information
+ * regarding copyright ownership. Some portions may be licensed
+ * to Red Hat, Inc. under one or more contributor license agreements.
+ * See the AUTHORS.txt file in the distribution for a full listing of
+ * individual contributors.
+ *
+ * ModeShape is free software. Unless otherwise indicated, all code in ModeShape
+ * is licensed to you under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * ModeShape is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this software; if not, write to the Free
+ * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
+ */
+package org.modeshape.graph.connector.base;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.ListIterator;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeMap;
+import java.util.UUID;
+import net.jcip.annotations.NotThreadSafe;
+import org.modeshape.common.util.StringUtil;
+import org.modeshape.graph.GraphI18n;
+import org.modeshape.graph.Location;
+import org.modeshape.graph.connector.RepositorySourceException;
+import org.modeshape.graph.connector.base.PathWorkspace.ChangeCommand;
+import org.modeshape.graph.property.Name;
+import org.modeshape.graph.property.Path;
+import org.modeshape.graph.property.PathNotFoundException;
+import org.modeshape.graph.property.Property;
+import org.modeshape.graph.property.Path.Segment;
+import org.modeshape.graph.query.QueryResults;
+import org.modeshape.graph.request.AccessQueryRequest;
+import org.modeshape.graph.request.FullTextSearchRequest;
+
+/**
+ * An implementation of {@link Transaction} that maintains a cache of nodes by their path.
+ *
+ * @param the type of workspace
+ * @param the type of node
+ */
+@NotThreadSafe
+public abstract class PathTransaction>
+ extends BaseTransaction {
+
+ /** The set of changes to the workspaces that have been made by this transaction */
+ private Map changesByWorkspaceName;
+
+ /**
+ * Create a new transaction.
+ *
+ * @param repository the repository against which the transaction will be operating; may not be null
+ * @param rootNodeUuid the UUID of the root node; may not be null
+ */
+ protected PathTransaction( Repository repository,
+ UUID rootNodeUuid ) {
+ super(repository.getContext(), repository, rootNodeUuid);
+ }
+
+ /**
+ * Get the changes for the supplied workspace, optionally creating the necessary object if it does not yet exist. The changes
+ * object is used to record the changes made to the workspace by operations within this transaction, which are either pushed
+ * into the workspace upon {@link #commit()} or cleared upon {@link #rollback()}.
+ *
+ * @param workspace the workspace
+ * @param createIfMissing true if the changes object should be created if it does not yet exist, or false otherwise
+ * @return the changes object; may be null if createIfMissing
is false
and the changes object does
+ * not yet exist, or never null if createIfMissing
is true
+ */
+ protected WorkspaceChanges getChangesFor( WorkspaceType workspace,
+ boolean createIfMissing ) {
+ if (changesByWorkspaceName == null) {
+ if (!createIfMissing) return null;
+ WorkspaceChanges changes = new WorkspaceChanges(workspace);
+ changesByWorkspaceName = new HashMap();
+ changesByWorkspaceName.put(workspace.getName(), changes);
+ return changes;
+ }
+ WorkspaceChanges changes = changesByWorkspaceName.get(workspace.getName());
+ if (changes == null && createIfMissing) {
+ changes = new WorkspaceChanges(workspace);
+ changesByWorkspaceName.put(workspace.getName(), changes);
+ }
+ return changes;
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @see org.modeshape.graph.connector.base.Transaction#getNode(org.modeshape.graph.connector.base.Workspace,
+ * org.modeshape.graph.Location)
+ */
+ public NodeType getNode( WorkspaceType workspace,
+ Location location ) {
+ assert location != null;
+ // First look for the UUID ...
+ UUID uuid = location.getUuid();
+ if (getRepository().getRootNodeUuid().equals(uuid)) {
+ Path rootPath = pathFactory.createRootPath();
+
+ // The root node can't be removed
+ WorkspaceChanges changes = getChangesFor(workspace, false);
+ NodeType node = null;
+ if (changes != null) {
+ assert !changes.isRemoved(rootPath);
+ // Not deleted, but maybe changed in this transaction ...
+ node = changes.getChangedOrAdded(rootPath);
+ if (node != null) return node;
+ }
+ // It hasn't been loaded already, so attempt to load it from the map owned by the workspace ...
+ node = workspace.getNode(rootPath);
+ if (node != null) return node;
+ }
+ // Otherwise, look by path ...
+ if (location.hasPath()) {
+ return getNode(workspace, location.getPath(), location);
+ }
+ // Unable to find by UUID or by path, so fail ...
+ Path lowestExisting = pathFactory.createRootPath();
+ throw new PathNotFoundException(location, lowestExisting, GraphI18n.nodeDoesNotExist.text(readable(location)));
+ }
+
+ /**
+ * Attempt to find the node with the supplied path. This method is "Changes-aware". That is, it checks the cache of changes
+ * for the workspace before returning the node.
+ *
+ * @param workspace the workspace; may not be null
+ * @param path the path of the node; may not be null
+ * @return the node, or null if no such node exists
+ */
+ protected NodeType findNode( WorkspaceType workspace,
+ Path path ) {
+ WorkspaceChanges changes = getChangesFor(workspace, false);
+ NodeType node = null;
+ if (changes != null) {
+ // See if the node we're looking for was deleted ...
+ if (changes.isRemoved(path)) {
+ // This node was removed within this transaction ...
+ return null;
+ }
+ // Not deleted, but maybe changed in this transaction ...
+ node = changes.getChangedOrAdded(path);
+ if (node != null) return node;
+ }
+ // It hasn't been loaded already, so attempt to load it from the map owned by the workspace ...
+ node = workspace.getNode(path);
+ return node;
+ }
+
+ /**
+ * Destroy the node.
+ *
+ * @param workspace the workspace; never null
+ * @param node the node to be destroyed
+ */
+ protected void destroyNode( WorkspaceType workspace,
+ NodeType node ) {
+ WorkspaceChanges changes = getChangesFor(workspace, true);
+ destroyNode(changes, workspace, node);
+ }
+
+ /**
+ * Destroy the node and it's contents.
+ *
+ * @param changes the record of the workspace changes; never null
+ * @param workspace the workspace; never null
+ * @param node the node to be destroyed
+ */
+ private void destroyNode( WorkspaceChanges changes,
+ WorkspaceType workspace,
+ NodeType node ) {
+
+ changes.removed(pathTo(node));
+ }
+
+ /**
+ * Returns the location for the given node, based on its path
+ *
+ * @param node the node for which the location should be returned; may not be null
+ * @return the location for the given node, based on its path; never null
+ */
+ private Location locationFor( NodeType node ) {
+ return Location.create(pathTo(node));
+ }
+
+ /**
+ * Returns the path to the given node
+ *
+ * @param node the node for which the path should be returned; may not be null
+ * @return the path to that node
+ */
+ protected Path pathTo( NodeType node ) {
+ if (node.getParent() == null) {
+ return pathFactory.createRootPath();
+ }
+ return pathFactory.create(node.getParent(), node.getName());
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @see org.modeshape.graph.connector.base.Transaction#addChild(org.modeshape.graph.connector.base.Workspace,
+ * org.modeshape.graph.connector.base.Node, org.modeshape.graph.property.Name, int, java.util.UUID, java.lang.Iterable)
+ */
+ @SuppressWarnings( "unchecked" )
+ public NodeType addChild( WorkspaceType workspace,
+ NodeType parent,
+ Name name,
+ int index,
+ UUID uuid,
+ Iterable properties ) {
+ assert uuid == null : "UUID should always be null for a PathTransaction";
+ WorkspaceChanges changes = getChangesFor(workspace, true);
+
+ // If the parent doesn't already have changes, we need to find the new parent in the newWorkspace's changes
+ if (!parent.hasChanges()) {
+ parent = getNode(workspace, locationFor(parent));
+ }
+
+ NodeType newNode = null;
+ if (index < 0) {
+ // Figure out the SNS of the new node ...
+ int snsIndex = 1;
+ for (NodeType child : getChildren(workspace, parent)) {
+ if (child.getName().getName().equals(name)) ++snsIndex;
+ }
+
+ // Create the new node ...
+ Segment childSegment = pathFactory.createSegment(name, snsIndex);
+ newNode = createNode(childSegment, pathTo(parent), properties);
+ // And add to the parent ...
+ parent = (NodeType)parent.withChild(childSegment);
+ } else {
+ int snsIndex = 0;
+ List children = getChildren(workspace, parent);
+
+ if (index < children.size()) {
+ ListIterator existingSiblings = children.listIterator(index);
+ while (existingSiblings.hasNext()) {
+ NodeType existingSibling = existingSiblings.next();
+ Segment existingSegment = existingSibling.getName();
+ if (existingSegment.getName().equals(name)) {
+ int existingIndex = existingSegment.getIndex();
+ if (snsIndex == 0) snsIndex = existingIndex;
+ existingSibling = (NodeType)existingSibling.withName(pathFactory.createSegment(name, existingIndex + 1));
+ changes.changed(existingSibling);
+ }
+ }
+ }
+ // Create the new node ...
+ Segment childSegment = pathFactory.createSegment(name, snsIndex + 1);
+ newNode = createNode(childSegment, pathTo(parent), properties);
+ // And add to the parent ...
+ parent = (NodeType)parent.withChild(index, childSegment);
+ }
+ changes.created(newNode);
+ changes.changed(parent);
+ return newNode;
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @see org.modeshape.graph.connector.base.Transaction#addChild(org.modeshape.graph.connector.base.Workspace,
+ * org.modeshape.graph.connector.base.Node, org.modeshape.graph.connector.base.Node,
+ * org.modeshape.graph.connector.base.Node, org.modeshape.graph.property.Name)
+ */
+ @SuppressWarnings( "unchecked" )
+ public Location addChild( WorkspaceType workspace,
+ NodeType parent,
+ NodeType newChild,
+ NodeType beforeOtherChild,
+ Name desiredName ) {
+ // If the parent doesn't already have changes, we need to find the new parent in the newWorkspace's changes
+ if (!parent.hasChanges()) {
+ parent = findNode(workspace, pathTo(parent));
+ }
+
+ // Get some information about the child ...
+ Segment newChildSegment = newChild.getName();
+ Name newChildName = newChildSegment.getName();
+ int snsIndex = newChildSegment.getIndex();
+
+ // Find the existing parent of the new child ...
+ NodeType oldParent = getParent(workspace, newChild);
+
+ // Find the changes for this workspace ...
+ WorkspaceChanges changes = getChangesFor(workspace, true);
+ NodeType newChildWithOldParent = null;
+
+ if (oldParent != null) {
+ newChildWithOldParent = newChild;
+ // Remove the node from it's parent ...
+ int oldIndex = oldParent.getChildren().indexOf(newChildSegment);
+ if (oldParent.equals(parent)) {
+ oldParent = (NodeType)oldParent.withoutChild(newChildSegment);
+ changes.changed(oldParent);
+ parent = oldParent;
+ } else {
+ oldParent = (NodeType)oldParent.withoutChild(newChildSegment);
+ changes.changed(oldParent);
+ }
+
+ // Now find any siblings with the same name that appear after the node in the parent's list of children ...
+ List siblings = getChildren(workspace, oldParent);
+ if (oldIndex < siblings.size()) {
+ for (ListIterator iter = siblings.listIterator(oldIndex); iter.hasNext();) {
+ NodeType sibling = iter.next();
+ if (sibling.getName().getName().equals(newChildName)) {
+ sibling = (NodeType)sibling.withName(pathFactory.createSegment(newChildName, snsIndex++));
+ changes.changed(sibling);
+ }
+ }
+ }
+ }
+
+ // Find the index of the other child ...
+ int index = parent.getChildren().size();
+ if (beforeOtherChild != null) {
+ if (!beforeOtherChild.getParent().equals(pathTo(parent))) {
+ // The other child doesn't exist in the parent ...
+ throw new RepositorySourceException(null);
+ }
+ Segment otherChild = beforeOtherChild.getName();
+ index = parent.getChildren().indexOf(otherChild);
+ }
+
+ // Determine the desired new name for the node ...
+ newChildName = desiredName != null ? desiredName : newChildName;
+
+ // Find the SNS index for the new child ...
+ ListIterator existingSiblings = getChildren(workspace, parent).listIterator(); // makes a copy
+ int i = 0;
+ snsIndex = 1;
+ Segment childName = null;
+ while (existingSiblings.hasNext()) {
+ NodeType existingSibling = existingSiblings.next();
+ Segment existingSegment = existingSibling.getName();
+ if (i < index) {
+ // Nodes before the insertion point
+ if (existingSegment.getName().equals(newChildName)) {
+ ++snsIndex;
+ }
+ } else {
+ if (i == index) {
+ // Add the child node ...
+ childName = pathFactory.createSegment(newChildName, snsIndex);
+ }
+ if (existingSegment.getName().equals(newChildName)) {
+ existingSibling = (NodeType)existingSibling.withName(pathFactory.createSegment(newChildName, ++snsIndex));
+ changes.changed(existingSibling);
+ }
+ }
+ ++i;
+ }
+ if (childName == null) {
+ // Must be appending the child ...
+ childName = pathFactory.createSegment(newChildName, snsIndex);
+ }
+
+ // Change the name of the new node ...
+ newChild = (NodeType)newChild.withName(childName).withParent(pathTo(parent));
+ parent = (NodeType)parent.withChild(index, newChild.getName());
+ changes.moved(newChildWithOldParent, newChild, parent);
+
+ return locationFor(newChild);
+ }
+
+ /**
+ * Create a new instance of the node, given the supplied name and parent path. This method should do nothing but instantiate
+ * the new node; the caller will add to the appropriate internal structures.
+ *
+ * @param name the name of the new node; may be null if this is the root node
+ * @param parentPath the path of the parent node; may be null if this is the root node
+ * @param properties the properties; may be null if there are no properties
+ * @return the new node; never null
+ */
+ protected abstract NodeType createNode( Segment name,
+ Path parentPath,
+ Iterable properties );
+
+ /**
+ * {@inheritDoc}
+ *
+ * @see org.modeshape.graph.connector.base.Transaction#getChild(org.modeshape.graph.connector.base.Workspace,
+ * org.modeshape.graph.connector.base.Node, org.modeshape.graph.property.Path.Segment)
+ */
+ @SuppressWarnings( "unchecked" )
+ public NodeType getChild( WorkspaceType workspace,
+ NodeType parent,
+ Segment childSegment ) {
+ List children = parent.getChildren(); // don't make a copy
+ for (Segment child : children) {
+ if (child.equals(childSegment)) {
+ Path childPath = pathFactory.create(pathTo(parent), child);
+
+ WorkspaceChanges changes = getChangesFor(workspace, true);
+ NodeType changed = changes.getChangedOrAdded(childPath);
+ if (changed != null) {
+ return changed;
+ }
+
+ Path persistentPath = changes.persistentPathFor(childPath);
+ NodeType childNode = workspace.getNode(persistentPath);
+
+ if (persistentPath.equals(childPath)) return childNode;
+
+ return (NodeType)childNode.withParent(childPath.getParent()).withName(childPath.getLastSegment());
+ }
+ }
+ return null;
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @see org.modeshape.graph.connector.base.Transaction#getChildren(org.modeshape.graph.connector.base.Workspace,
+ * org.modeshape.graph.connector.base.Node)
+ */
+ public List getChildren( WorkspaceType workspace,
+ NodeType node ) {
+ List childSegments = node.getChildren(); // make a copy
+ if (childSegments.isEmpty()) return Collections.emptyList();
+
+ List children = new ArrayList(childSegments.size());
+
+ for (Segment childSegment : childSegments) {
+ children.add(getNode(workspace, Location.create(pathFactory.create(pathTo(node), childSegment))));
+ }
+
+ return children;
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @see org.modeshape.graph.connector.base.Transaction#getParent(org.modeshape.graph.connector.base.Workspace,
+ * org.modeshape.graph.connector.base.Node)
+ */
+ public NodeType getParent( WorkspaceType workspace,
+ NodeType node ) {
+ Path parentPath = node.getParent();
+ if (parentPath == null) return null;
+ return getNode(workspace, Location.create(parentPath));
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @see org.modeshape.graph.connector.base.Transaction#removeAllChildren(org.modeshape.graph.connector.base.Workspace,
+ * org.modeshape.graph.connector.base.Node)
+ */
+ @SuppressWarnings( "unchecked" )
+ public void removeAllChildren( WorkspaceType workspace,
+ NodeType node ) {
+ for (NodeType child : getChildren(workspace, node)) {
+ destroyNode(workspace, child);
+ }
+ node = (NodeType)node.withoutChildren();
+ getChangesFor(workspace, true).changed(node);
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @see org.modeshape.graph.connector.base.Transaction#removeNode(org.modeshape.graph.connector.base.Workspace,
+ * org.modeshape.graph.connector.base.Node)
+ */
+ @SuppressWarnings( "unchecked" )
+ public Location removeNode( WorkspaceType workspace,
+ NodeType node ) {
+ NodeType parent = getParent(workspace, node);
+ if (parent == null) {
+ // The root node is being removed, which means we should just delete everything (except the root) ...
+ WorkspaceChanges changes = getChangesFor(workspace, true);
+ changes.removeAll(null);
+ return Location.create(pathFactory.createRootPath(), rootNodeUuid);
+ }
+ Location result = locationFor(node);
+
+ // Find the index of the node in it's parent ...
+ int index = parent.getChildren().indexOf(node.getName());
+ assert index != -1;
+ Name name = node.getName().getName();
+ int snsIndex = node.getName().getIndex();
+ WorkspaceChanges changes = getChangesFor(workspace, true);
+ // Remove the node from the parent ...
+ parent = (NodeType)parent.withoutChild(node.getName());
+ changes.changed(parent);
+
+ // Now find any siblings with the same name that appear after the node in the parent's list of children ...
+ List siblings = getChildren(workspace, parent);
+ if (index < siblings.size()) {
+ for (ListIterator iter = siblings.listIterator(index); iter.hasNext();) {
+ NodeType sibling = iter.next();
+ if (sibling.getName().getName().equals(name)) {
+ sibling = (NodeType)sibling.withName(pathFactory.createSegment(name, snsIndex++));
+ changes.changed(sibling);
+ }
+ }
+ }
+ // Destroy the subgraph starting at the node, and record the change on the parent ...
+ destroyNode(changes, workspace, node);
+ return result;
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @see org.modeshape.graph.connector.base.Transaction#removeProperty(org.modeshape.graph.connector.base.Workspace,
+ * org.modeshape.graph.connector.base.Node, org.modeshape.graph.property.Name)
+ */
+ @SuppressWarnings( "unchecked" )
+ public NodeType removeProperty( WorkspaceType workspace,
+ NodeType node,
+ Name propertyName ) {
+ NodeType copy = (NodeType)node.withoutProperty(propertyName);
+ if (copy != node) {
+ WorkspaceChanges changes = getChangesFor(workspace, true);
+ changes.changed(copy);
+ }
+ return copy;
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @see org.modeshape.graph.connector.base.Transaction#setProperties(org.modeshape.graph.connector.base.Workspace,
+ * org.modeshape.graph.connector.base.Node, java.lang.Iterable, java.lang.Iterable, boolean)
+ */
+ @SuppressWarnings( "unchecked" )
+ public NodeType setProperties( WorkspaceType workspace,
+ NodeType node,
+ Iterable propertiesToSet,
+ Iterable propertiesToRemove,
+ boolean removeAllExisting ) {
+ NodeType copy = (NodeType)node.withProperties(propertiesToSet, propertiesToRemove, removeAllExisting);
+ if (copy != node) {
+ WorkspaceChanges changes = getChangesFor(workspace, true);
+ changes.changed(copy);
+ }
+ return copy;
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @see org.modeshape.graph.connector.base.Transaction#copyNode(org.modeshape.graph.connector.base.Workspace,
+ * org.modeshape.graph.connector.base.Node, org.modeshape.graph.connector.base.Workspace,
+ * org.modeshape.graph.connector.base.Node, org.modeshape.graph.property.Name, boolean)
+ */
+ @SuppressWarnings( "unchecked" )
+ public NodeType copyNode( WorkspaceType originalWorkspace,
+ NodeType original,
+ WorkspaceType newWorkspace,
+ NodeType newParent,
+ Name desiredName,
+ boolean recursive ) {
+ if (desiredName == null) desiredName = original.getName().getName();
+ // Create a copy of the original under the new parent ...
+ NodeType copy = addChild(newWorkspace, newParent, desiredName, -1, null, original.getProperties().values());
+
+ if (recursive) {
+ WorkspaceChanges changes = getChangesFor(newWorkspace, true);
+ // Walk through the original branch in its workspace ...
+ for (NodeType originalChild : getChildren(originalWorkspace, original)) {
+ NodeType newChild = copyBranch(originalWorkspace, originalChild, changes, newWorkspace, copy);
+ copy = (NodeType)copy.withChild(newChild.getName());
+ }
+ }
+
+ // Record the latest changes on the newly-created node ..
+ WorkspaceChanges changes = getChangesFor(newWorkspace, true);
+ changes.changed(copy);
+
+ return copy;
+ }
+
+ protected void print( WorkspaceType workspace,
+ NodeType node,
+ int level ) {
+ StringBuilder sb = new StringBuilder();
+ sb.append(StringUtil.createString(' ', level * 2));
+ sb.append(readable(node.getName())).append(" (").append(node.getUuid()).append(") {");
+ boolean first = true;
+ for (Property property : node.getProperties().values()) {
+ if (first) first = false;
+ else sb.append(',');
+ sb.append(readable(property.getName())).append('=');
+ if (property.isMultiple()) sb.append(property.getValuesAsArray());
+ else sb.append(readable(property.getFirstValue()));
+ }
+ sb.append('}');
+ System.out.println(sb);
+ for (NodeType child : getChildren(workspace, node)) {
+ print(workspace, child, level + 1);
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @see org.modeshape.graph.connector.base.Transaction#cloneNode(org.modeshape.graph.connector.base.Workspace,
+ * org.modeshape.graph.connector.base.Node, org.modeshape.graph.connector.base.Workspace,
+ * org.modeshape.graph.connector.base.Node, org.modeshape.graph.property.Name, org.modeshape.graph.property.Path.Segment,
+ * boolean, java.util.Set)
+ */
+ public NodeType cloneNode( WorkspaceType originalWorkspace,
+ NodeType original,
+ WorkspaceType newWorkspace,
+ NodeType newParent,
+ Name desiredName,
+ Segment desiredSegment,
+ boolean removeExisting,
+ java.util.Set removedExistingNodes )
+ throws org.modeshape.graph.connector.UuidAlreadyExistsException {
+
+ return copyNode(originalWorkspace, original, newWorkspace, newParent, desiredName, true);
+ }
+
+ @SuppressWarnings( "unchecked" )
+ protected NodeType copyBranch( WorkspaceType originalWorkspace,
+ NodeType original,
+ WorkspaceChanges newWorkspaceChanges,
+ WorkspaceType newWorkspace,
+ NodeType newParent ) {
+ // Create the new node (or reuse the original if we can) ...
+ NodeType copy = createNode(original.getName(), pathTo(newParent), original.getProperties().values());
+ newWorkspaceChanges.created(copy);
+
+ // Walk through the children and call this method recursively ...
+ for (NodeType originalChild : getChildren(originalWorkspace, original)) {
+ NodeType newChild = copyBranch(originalWorkspace, originalChild, newWorkspaceChanges, newWorkspace, copy);
+ copy = (NodeType)copy.withChild(newChild.getName());
+ }
+ newWorkspaceChanges.changed(copy);
+ return copy;
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * This implementation does not support querying the repository contents, so this method returns null. Subclasses can override
+ * this if they do support querying.
+ *
+ *
+ * @see org.modeshape.graph.connector.base.Transaction#query(org.modeshape.graph.connector.base.Workspace,
+ * org.modeshape.graph.request.AccessQueryRequest)
+ */
+ public QueryResults query( WorkspaceType workspace,
+ AccessQueryRequest accessQuery ) {
+ return null;
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * This implementation does not support searching the repository contents, so this method returns null. Subclasses can
+ * override this if they do support searching.
+ *
+ *
+ * @see org.modeshape.graph.connector.base.Transaction#search(org.modeshape.graph.connector.base.Workspace,
+ * org.modeshape.graph.request.FullTextSearchRequest)
+ */
+ public QueryResults search( WorkspaceType workspace,
+ FullTextSearchRequest search ) {
+ return null;
+ }
+
+ protected void validateNode( WorkspaceType workspace,
+ NodeType node ) {
+
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @see org.modeshape.graph.connector.base.BaseTransaction#commit()
+ */
+ @Override
+ public void commit() {
+ super.commit();
+ // Push all of the changes or added nodes onto the workspace ...
+ if (changesByWorkspaceName != null) {
+ for (WorkspaceChanges changes : changesByWorkspaceName.values()) {
+ changes.commit();
+ }
+ changesByWorkspaceName.clear();
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @see org.modeshape.graph.connector.base.BaseTransaction#rollback()
+ */
+ @Override
+ public void rollback() {
+ super.rollback();
+ if (changesByWorkspaceName != null) {
+ changesByWorkspaceName.clear();
+ }
+ }
+
+ /**
+ * Record of the changes made to a particular workspace.
+ */
+ protected class WorkspaceChanges {
+ private final WorkspaceType workspace;
+ private final Map changedOrAddedNodes = new TreeMap();
+ private final TreeMap movedNodes = new TreeMap();
+ private final Set removedNodes = new HashSet();
+ private final List> commands = new LinkedList>();
+
+ protected WorkspaceChanges( WorkspaceType workspace ) {
+ this.workspace = workspace;
+ }
+
+ public WorkspaceType getWorkspace() {
+ return workspace;
+ }
+
+ public void removeAll( NodeType newRootNode ) {
+ changedOrAddedNodes.clear();
+ removedNodes.clear();
+ commands.add(workspace.createRemoveCommand(pathFactory.createRootPath()));
+ }
+
+ public boolean isRemoved( Path path ) {
+ return removedNodes.contains(path);
+ }
+
+ public Path persistentPathFor( Path path ) {
+ for (Path newPath : movedNodes.descendingKeySet()) {
+ if (path.isAtOrBelow(newPath)) {
+ return path.relativeTo(newPath).resolveAgainst(movedNodes.get(newPath));
+ }
+ }
+ return path;
+ }
+
+ public NodeType getChangedOrAdded( Path path ) {
+ return changedOrAddedNodes.get(path);
+ }
+
+ public void removed( Path path ) {
+ removedNodes.add(path);
+ changedOrAddedNodes.remove(path);
+ commands.add(workspace.createRemoveCommand(path));
+ }
+
+ public void created( NodeType node ) {
+ validateNode(workspace, node);
+
+ Path path = pathTo(node);
+ removedNodes.remove(path);
+ changedOrAddedNodes.put(path, node);
+ commands.add(workspace.createPutCommand(node));
+ }
+
+ public void changed( NodeType node ) {
+ validateNode(workspace, node);
+
+ Path path = pathTo(node);
+ assert !removedNodes.contains(path); // should not be removed
+ changedOrAddedNodes.put(path, node);
+ commands.add(workspace.createPutCommand(node));
+ }
+
+ public void moved( NodeType node,
+ NodeType newNode,
+ NodeType newParent ) {
+ validateNode(workspace, newNode);
+
+ Path path = pathTo(node);
+ Path newPath = pathTo(newNode);
+ changedOrAddedNodes.put(newPath, newNode);
+ changedOrAddedNodes.put(pathTo(newParent), newParent);
+ movedNodes.put(pathTo(newNode), path);
+ commands.add(workspace.createMoveCommand(node, newNode));
+ }
+
+ public void commit() {
+ workspace.commit(commands);
+ }
+
+ @Override
+ public String toString() {
+ return changedOrAddedNodes.keySet().toString() + "\n\t" + movedNodes.toString();
+ }
+ }
+}
Index: modeshape-graph/src/main/java/org/modeshape/graph/connector/base/PathWorkspace.java
new file mode 100644
===================================================================
--- /dev/null (revision 1823)
+++ modeshape-graph/src/main/java/org/modeshape/graph/connector/base/PathWorkspace.java (working copy)
@@ -0,0 +1,305 @@
+/*
+ * ModeShape (http://www.modeshape.org)
+ * See the COPYRIGHT.txt file distributed with this work for information
+ * regarding copyright ownership. Some portions may be licensed
+ * to Red Hat, Inc. under one or more contributor license agreements.
+ * See the AUTHORS.txt file in the distribution for a full listing of
+ * individual contributors.
+ *
+ * ModeShape is free software. Unless otherwise indicated, all code in ModeShape
+ * is licensed to you under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * ModeShape is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this software; if not, write to the Free
+ * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
+ */
+package org.modeshape.graph.connector.base;
+
+import java.util.List;
+import java.util.UUID;
+import java.util.concurrent.locks.ReadWriteLock;
+import net.jcip.annotations.NotThreadSafe;
+import org.modeshape.graph.property.Path;
+
+/**
+ * The {@link Workspace} implementation that represents all nodes as {@link PathNode} objects and stores them in an internal data
+ * structure that allows for nodes to be accessed via a {@link Path}.
+ *
+ * Subclasses are required to provide thread-safe access and modification of the state within the encapsulated data structure,
+ * since multiple {@link Transaction} implementations may be {@link Transaction#commit() committing} changes to the data structure
+ * at the same time. However, this class does not provide any thread-safety, since the nature of the thread-safety will almost
+ * certainly depend on the actual implementation. For example, a subclass may use a {@link ReadWriteLock lock}, or it may use a
+ * map implementation that provides the thread-safety.
+ *
+ *
+ * @param the type of node
+ */
+@NotThreadSafe
+public abstract class PathWorkspace implements Workspace {
+
+ private final String name;
+ private final UUID rootNodeUuid;
+
+ /**
+ * Create a new instance of the workspace.
+ *
+ * @param name the workspace name; may not be null
+ * @param rootNodeUuid the root node that is expected to already exist in the map
+ */
+ public PathWorkspace( String name,
+ UUID rootNodeUuid ) {
+ this.name = name;
+ this.rootNodeUuid = rootNodeUuid;
+ assert this.name != null;
+ assert this.rootNodeUuid != null;
+ }
+
+ /**
+ * Create a new instance of the workspace.
+ *
+ * @param name the workspace name; may not be null
+ * @param originalToClone the workspace that is to be cloned; may not be null
+ */
+ public PathWorkspace( String name,
+ PathWorkspace originalToClone ) {
+ this.name = name;
+ this.rootNodeUuid = originalToClone.getRootNode().getUuid();
+ assert this.name != null;
+ assert this.rootNodeUuid != null;
+ throw new UnsupportedOperationException("Need to implement the ability to clone a workspace");
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @see org.modeshape.graph.connector.base.Workspace#getName()
+ */
+ public String getName() {
+ return name;
+ }
+
+ protected UUID getRootNodeUuid() {
+ return rootNodeUuid;
+ }
+
+ /**
+ * Get the root node in this workspace.
+ *
+ * @return the root node; never null
+ */
+ public abstract NodeType getRootNode();
+
+ /**
+ * Get the node with the supplied path.
+ *
+ * @param path the path to the node
+ * @return the node state as known by this workspace, or null if no such node exists in this workspace
+ */
+ public abstract NodeType getNode( Path path );
+
+ /**
+ * Save this node into the workspace, overwriting any previous record of the node. This method should be overridden by
+ * writable path workspace implementations that use the default {@link ChangeCommand} implementations.
+ *
+ * @param node the new node; may not be null
+ * @return the previous node state, or null if the node is new to this workspace
+ * @throws UnsupportedOperationException by default, subclasses should override this method so that this exception is not
+ * thrown
+ * @see #createMoveCommand(PathNode, PathNode)
+ * @see #createPutCommand(PathNode)
+ * @see #createRemoveCommand(Path)
+ */
+ public NodeType putNode( NodeType node ) {
+ throw new UnsupportedOperationException();
+ }
+
+ /**
+ * Move the node from it's previous location to the new location, overwriting any previous node at that location. This method
+ * should be overridden by writable path workspace implementations that use the default {@link ChangeCommand} implementations.
+ *
+ * The move operation is intended to reflect changes to the node's {@link PathNode#getName() name} or
+ * {@link PathNode#getParent() parent} only. Changes to the children or properties of the node should be reflected separately
+ * in a {@link #putNode(PathNode) put command} of some sort. The details of the put command are implementation-specific.
+ *
+ *
+ * @param source the original version of the node to be moved; may not be null
+ * @param target the new version (implying a change to the name or parent) of the node to be moved; may not be null
+ * @return the new node state;may not be null
+ * @throws UnsupportedOperationException by default, subclasses should override this method so that this exception is not
+ * thrown
+ * @see #createMoveCommand(PathNode, PathNode)
+ * @see #createPutCommand(PathNode)
+ * @see #createRemoveCommand(Path)
+ */
+ public NodeType moveNode( NodeType source,
+ NodeType target ) {
+ throw new UnsupportedOperationException();
+ }
+
+ /**
+ * Remove this node and its descendants from the workspace. This method should be overridden by writable path workspace
+ * implementations that use the default {@link ChangeCommand} implementations.
+ *
+ * @param path the path to the node to be removed; may not be null
+ * @return the previous node state, or null if the node does not exist in this workspace
+ * @throws UnsupportedOperationException by default, subclasses should override this method so that this exception is not
+ * thrown
+ * @see #createMoveCommand(PathNode, PathNode)
+ * @see #createPutCommand(PathNode)
+ * @see #createRemoveCommand(Path)
+ */
+ public NodeType removeNode( Path path ) {
+ throw new UnsupportedOperationException();
+ }
+
+ /**
+ * Remove all of the nodes in this workspace, and make sure there is a single root node with no properties and no children.
+ */
+ public void removeAll() {
+ throw new UnsupportedOperationException();
+
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @see java.lang.Object#toString()
+ */
+ @Override
+ public String toString() {
+ return name;
+ }
+
+ /**
+ * Successively (and in order) apply the changes from the list of pending commands
+ *
+ * All validation for each of the objects (including validation of resource availability in the underlying persistent store)
+ * should be performed prior to invoking this method.
+ *
+ *
+ * @param commands the list of commands to apply
+ */
+ public void commit( List> commands ) {
+ for (ChangeCommand command : commands) {
+ command.apply();
+ }
+ }
+
+ /**
+ * Create a change command for the required update to the given node
+ *
+ * @param node the new version of the node; may not be null
+ * @return a {@link ChangeCommand} instance that reflects the changes to the node
+ * @see #createPutCommand(PathNode)
+ * @see #commit(List)
+ */
+ public ChangeCommand createPutCommand( NodeType node ) {
+ return new PutCommand(node);
+ }
+
+ /**
+ * Create a change command for the removal of the given node and its descendants
+ *
+ * @param path the path to the node at the root of the branch to be removed; may not be null
+ * @return a {@link ChangeCommand} instance that reflects the changes to the node
+ * @see #createPutCommand(PathNode)
+ * @see #commit(List)
+ */
+ public ChangeCommand createRemoveCommand( Path path ) {
+ return new RemoveCommand(path);
+ }
+
+ /**
+ * Create a change command that represents the movement of a node. The movement record will only reflect the changes to the
+ * node's name and/or parent. Changes to the node's properties or children should be ignored. A separate
+ * {@link #createPutCommand(PathNode) put command} should be used to reflect these changes.
+ *
+ * @param source the original version of the node; may not be null
+ * @param target the new version of the node; may not be null
+ * @return a {@link ChangeCommand} instance that reflects the changes to the node
+ * @see #createMoveCommand(PathNode, PathNode)
+ * @see #commit(List)
+ */
+ public ChangeCommand createMoveCommand( NodeType source,
+ NodeType target ) {
+ return new MoveCommand(source, target);
+ }
+
+ /**
+ * A specific operation that mutates the underlying persistent repository.
+ *
+ * @param the type of node against which this change should apply
+ */
+ public interface ChangeCommand {
+ /**
+ * Make the change represented by this command permanent.
+ */
+ void apply();
+ }
+
+ private class PutCommand implements ChangeCommand {
+ private NodeType node;
+
+ protected PutCommand( NodeType node ) {
+ super();
+ this.node = node;
+ }
+
+ public void apply() {
+ PathWorkspace.this.putNode(node);
+ }
+
+ @Override
+ public String toString() {
+ return "Put: { " + node + "}";
+ }
+ }
+
+ private class RemoveCommand implements ChangeCommand {
+ private Path path;
+
+ protected RemoveCommand( Path path ) {
+ super();
+ this.path = path;
+ }
+
+ public void apply() {
+ PathWorkspace.this.removeNode(path);
+ }
+
+ @Override
+ public String toString() {
+ return "Remove: { " + path.getString() + "}";
+ }
+ }
+
+ private class MoveCommand implements ChangeCommand {
+ private NodeType node;
+ private NodeType newNode;
+
+ protected MoveCommand( NodeType node,
+ NodeType newNode ) {
+ super();
+ this.node = node;
+ this.newNode = newNode;
+ }
+
+ public void apply() {
+ PathWorkspace.this.moveNode(node, newNode);
+ }
+
+ @Override
+ public String toString() {
+ return "Move: { " + node + " to " + newNode + "}";
+ }
+ }
+
+}
Index: modeshape-graph/src/main/java/org/modeshape/graph/connector/base/Processor.java
===================================================================
--- modeshape-graph/src/main/java/org/modeshape/graph/connector/base/Processor.java (revision 1823)
+++ modeshape-graph/src/main/java/org/modeshape/graph/connector/base/Processor.java (working copy)
@@ -114,7 +114,15 @@ public class Processor e
for (Node child : children) {
Segment childName = child.getName();
Path childPath = pathFactory.create(path, childName);
- request.addChild(childPath, propertyFactory.create(ModeShapeLexicon.UUID, child.getUuid()));
+ Location childLocation = null;
+
+ if (child.getUuid() != null) {
+ childLocation = Location.create(childPath, child.getUuid());
+ } else {
+ childLocation = Location.create(childPath);
+ }
+
+ request.addChild(childLocation);
}
// Get the properties of the node ...
@@ -147,7 +155,14 @@ public class Processor e
for (Node child : children) {
Segment childName = child.getName();
Path childPath = pathFactory.create(path, childName);
- request.addChild(childPath, propertyFactory.create(ModeShapeLexicon.UUID, child.getUuid()));
+ Location childLocation = null;
+
+ if (child.getUuid() != null) {
+ childLocation = Location.create(childPath, child.getUuid());
+ } else {
+ childLocation = Location.create(childPath);
+ }
+ request.addChild(childLocation);
}
request.setActualLocationOfNode(actualLocation);
setCacheableInfo(request);
@@ -302,8 +317,13 @@ public class Processor e
// See if the node already exists (this doesn't record an error on the request) ...
node = txn.getFirstChild(workspace, parentNode, request.named());
if (node != null) {
- txn.removeNode(workspace, node);
- node = txn.addChild(workspace, parentNode, request.named(), 1, uuid, propsToStore);
+ List children = txn.getChildren(workspace, node);
+ for (NodeType child : children) {
+ txn.removeNode(workspace, child);
+ }
+ txn.setProperties(workspace, node, propsToStore, null, true);
+ // txn.removeNode(workspace, node);
+ // node = txn.addChild(workspace, parentNode, request.named(), 1, uuid, propsToStore);
} else {
node = txn.addChild(workspace, parentNode, request.named(), -1, uuid, propsToStore);
}
Index: modeshape-graph/src/main/java/org/modeshape/graph/connector/base/Transaction.java
===================================================================
--- modeshape-graph/src/main/java/org/modeshape/graph/connector/base/Transaction.java (revision 1823)
+++ modeshape-graph/src/main/java/org/modeshape/graph/connector/base/Transaction.java (working copy)
@@ -45,7 +45,7 @@ import org.modeshape.graph.request.LockBranchRequest.LockScope;
* A transaction in which all read and write operations against a repository are performed. The actual transaction instance is
* obtained by calling {@link Repository#startTransaction(ExecutionContext,boolean)}.
*
- * Note that implementations are not required to be thread-safe, since they (and their corresonding {@link Connection}) are
+ * Note that implementations are not required to be thread-safe, since they (and their corresponding {@link Connection}) are
* expected to be used by a single thread.
*
*
Index: modeshape-graph/src/main/java/org/modeshape/graph/connector/inmemory/InMemoryTransaction.java
===================================================================
--- modeshape-graph/src/main/java/org/modeshape/graph/connector/inmemory/InMemoryTransaction.java (revision 1823)
+++ modeshape-graph/src/main/java/org/modeshape/graph/connector/inmemory/InMemoryTransaction.java (working copy)
@@ -23,7 +23,6 @@
*/
package org.modeshape.graph.connector.inmemory;
-import java.util.Set;
import java.util.UUID;
import java.util.concurrent.locks.Lock;
import net.jcip.annotations.NotThreadSafe;
@@ -52,15 +51,6 @@ public class InMemoryTransaction extends MapTransaction getWorkspaceNames() {
- return repository.getWorkspaceNames();
- }
-
- /**
- * {@inheritDoc}
- *
* @see org.modeshape.graph.connector.base.Transaction#getWorkspace(java.lang.String,
* org.modeshape.graph.connector.base.Workspace)
*/
Index: modeshape-graph/src/test/java/org/modeshape/graph/connector/base/MockPathNode.java
new file mode 100644
===================================================================
--- /dev/null (revision 1823)
+++ modeshape-graph/src/test/java/org/modeshape/graph/connector/base/MockPathNode.java (working copy)
@@ -0,0 +1,58 @@
+package org.modeshape.graph.connector.base;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+import org.modeshape.graph.property.Name;
+import org.modeshape.graph.property.Path;
+import org.modeshape.graph.property.Property;
+import org.modeshape.graph.property.Path.Segment;
+
+@SuppressWarnings( "serial" )
+public class MockPathNode extends PathNode {
+
+ public MockPathNode( UUID uuid,
+ Path parent,
+ Segment name,
+ Iterable properties,
+ List children ) {
+ super(uuid, parent, name, properties, children);
+ }
+
+ public MockPathNode( UUID uuid,
+ Path parent,
+ Segment name,
+ Map properties,
+ List children,
+ int version ) {
+ super(uuid, parent, name, properties, children, version);
+ }
+
+ public MockPathNode( UUID uuid,
+ Path parent,
+ Segment name,
+ Map properties,
+ List children ) {
+ super(uuid, parent, name, properties, children);
+ }
+
+ public MockPathNode( UUID uuid ) {
+ super(uuid);
+ }
+
+ @Override
+ public PathNode clone() {
+ return new MockPathNode(getUuid(), getParent(), getName(), new HashMap(getProperties()),
+ new ArrayList(getChildren()));
+ }
+
+ @Override
+ public PathNode freeze() {
+ if (!hasChanges()) return this;
+ return new MockPathNode(getUuid(), changes.getParent(), changes.getName(), changes.getUnmodifiableProperties(),
+ changes.getUnmodifiableChildren(), getVersion() + 1);
+ }
+
+}
Index: modeshape-graph/src/test/java/org/modeshape/graph/connector/base/MockPathRepository.java
new file mode 100644
===================================================================
--- /dev/null (revision 1823)
+++ modeshape-graph/src/test/java/org/modeshape/graph/connector/base/MockPathRepository.java (working copy)
@@ -0,0 +1,64 @@
+package org.modeshape.graph.connector.base;
+
+import java.util.HashMap;
+import java.util.Map;
+import org.modeshape.graph.ExecutionContext;
+import org.modeshape.graph.property.Path;
+import org.modeshape.graph.property.Property;
+import org.modeshape.graph.property.Path.Segment;
+import org.modeshape.graph.request.InvalidWorkspaceException;
+
+public class MockPathRepository extends Repository {
+
+ protected final Map workspaces = new HashMap();
+
+ public MockPathRepository( BaseRepositorySource source ) {
+ super(source);
+ initialize();
+ }
+
+ @Override
+ public PathTransaction startTransaction( ExecutionContext context,
+ boolean readonly ) {
+ return new MockPathTransaction(this);
+ }
+
+ public class MockPathTransaction extends PathTransaction {
+
+ @Override
+ protected MockPathNode createNode( Segment name,
+ Path parentPath,
+ Iterable properties ) {
+ return new MockPathNode(null, parentPath, name, properties, null);
+ }
+
+ public MockPathTransaction( Repository repository ) {
+ super(repository, repository.getRootNodeUuid());
+ }
+
+ @Override
+ public boolean destroyWorkspace( MockPathWorkspace workspace ) throws InvalidWorkspaceException {
+ return false;
+ }
+
+ @Override
+ public MockPathWorkspace getWorkspace( String name,
+ MockPathWorkspace originalToClone ) throws InvalidWorkspaceException {
+ MockPathWorkspace workspace = workspaces.get(name);
+
+ if (workspace != null) {
+ return workspace;
+ }
+
+ if (originalToClone != null) {
+ workspace = new MockPathWorkspace(name, originalToClone);
+ } else {
+ workspace = new MockPathWorkspace(name, getRepository().getRootNodeUuid());
+ }
+
+ workspaces.put(name, workspace);
+ return workspace;
+ }
+
+ }
+}
Index: modeshape-graph/src/test/java/org/modeshape/graph/connector/base/MockPathWorkspace.java
new file mode 100644
===================================================================
--- /dev/null (revision 1823)
+++ modeshape-graph/src/test/java/org/modeshape/graph/connector/base/MockPathWorkspace.java (working copy)
@@ -0,0 +1,217 @@
+package org.modeshape.graph.connector.base;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+import org.modeshape.graph.ExecutionContext;
+import org.modeshape.graph.property.Name;
+import org.modeshape.graph.property.Path;
+import org.modeshape.graph.property.PathFactory;
+import org.modeshape.graph.property.Property;
+import org.modeshape.graph.property.Path.Segment;
+
+public class MockPathWorkspace extends PathWorkspace {
+
+ protected ExecutionContext context = new ExecutionContext();
+ protected InternalNode rootNode;
+
+ public MockPathWorkspace( String name,
+ UUID rootNodeUuid ) {
+ super(name, rootNodeUuid);
+ }
+
+ public MockPathWorkspace( String name,
+ MockPathWorkspace originalToClone ) {
+ super(name, originalToClone);
+ }
+
+ protected PathFactory pathFactory() {
+ return context.getValueFactories().getPathFactory();
+ }
+
+ private InternalNode nodeAt( Path path ) {
+ InternalNode node = rootNode();
+
+ for (Segment segment : path) {
+ node = node.getChild(segment);
+ if (node == null) return null;
+ }
+ return node;
+ }
+
+ @Override
+ public MockPathNode getNode( Path path ) {
+
+ if (nodeAt(path) == null) {
+ assert nodeAt(path) != null;
+ }
+
+ return pathNodeFor(nodeAt(path));
+ }
+
+ private InternalNode rootNode() {
+ if (rootNode == null) {
+ rootNode = new InternalNode(getRootNodeUuid());
+ }
+
+ return rootNode;
+ }
+
+ @Override
+ public MockPathNode getRootNode() {
+ return pathNodeFor(rootNode());
+ }
+
+ private MockPathNode pathNodeFor(InternalNode node) {
+ if (node.getParent() == null) {
+ new MockPathNode(node.getUuid(), null, node.getName(), node.getProperties(), node.getChildren());
+ }
+ return new MockPathNode(node.getUuid(), node.getPath().getParent(), node.getName(), node.getProperties(),
+ node.getChildren());
+ }
+
+ @Override
+ public MockPathNode putNode( MockPathNode node ) {
+ InternalNode target;
+ if (node.getParent() == null) {
+ target = rootNode;
+ }
+ else {
+ InternalNode parent = nodeAt(node.getParent());
+ target = parent.getChild(node.getName());
+
+ if (target == null) {
+ target = new InternalNode(parent, node.getName(), node.getProperties(), null);
+ parent.addChild(target);
+ return pathNodeFor(target);
+ }
+ }
+
+ target.setProperties(node.getProperties());
+
+ return pathNodeFor(target);
+ }
+
+ @Override
+ public MockPathNode moveNode( MockPathNode node,
+ MockPathNode newNode ) {
+ InternalNode parent = nodeAt(newNode.getParent());
+
+ InternalNode child = nodeAt(pathFactory().create(node.getParent(), node.getName()));
+
+ parent.addChild(child);
+
+ return pathNodeFor(child);
+ }
+
+ @Override
+ public void removeAll() {
+ rootNode = null;
+ }
+
+ @Override
+ public MockPathNode removeNode( Path path ) {
+ if (path.isRoot()) {
+ InternalNode oldRoot = rootNode;
+ removeAll();
+ return pathNodeFor(oldRoot);
+ }
+
+ InternalNode target = nodeAt(path);
+ InternalNode parent = target.getParent();
+
+ parent.removeChild(target.getName());
+
+ return pathNodeFor(target);
+ }
+
+ private class InternalNode {
+ private UUID uuid;
+ private Segment name;
+ private InternalNode parent;
+ private Map properties;
+ private List children;
+
+ protected InternalNode( UUID uuid ) {
+ this(uuid, null, null, null, null);
+ }
+
+ protected InternalNode( InternalNode parent,
+ Segment name,
+ Map properties,
+ List children ) {
+ this(null, parent, name, properties, children);
+ }
+
+ protected InternalNode( UUID uuid,
+ InternalNode parent,
+ Segment name,
+ Map properties,
+ List children ) {
+ this.uuid = uuid;
+ this.parent = parent;
+ this.name = name;
+ this.properties = properties == null ? new HashMap() : new HashMap(properties);
+ this.children = children == null ? new ArrayList() : children;
+ }
+
+ protected InternalNode getChild( Segment segment ) {
+ for (InternalNode node : children) {
+ if (node.getName().equals(segment)) return node;
+ }
+ return null;
+ }
+
+ protected void removeChild( Segment segment ) {
+ children.remove(segment);
+ }
+
+ protected void addChild( InternalNode child ) {
+ children.add(child);
+ child.parent = this;
+ }
+
+ protected List getChildren() {
+ List childSegments = new ArrayList(children.size());
+
+ for (InternalNode child : children) {
+ childSegments.add(child.getName());
+ }
+
+ return childSegments;
+ }
+
+ protected Map getProperties() {
+ return new HashMap(properties);
+ }
+
+ protected void setProperties( Map properties ) {
+ this.properties = new HashMap(properties);
+ }
+
+ protected InternalNode getParent() {
+ return parent;
+ }
+
+ protected Segment getName() {
+ return name;
+ }
+
+ protected UUID getUuid() {
+ return uuid;
+ }
+
+ protected Path getPath() {
+ if (parent == null) return pathFactory().createRootPath();
+
+ return pathFactory().create(parent.getPath(), name);
+ }
+
+ @Override
+ public String toString() {
+ return getPath().getString(context.getNamespaceRegistry());
+ }
+ }
+}
Index: modeshape-graph/src/test/java/org/modeshape/graph/connector/base/PathRepositoryTest.java
new file mode 100644
===================================================================
--- /dev/null (revision 1823)
+++ modeshape-graph/src/test/java/org/modeshape/graph/connector/base/PathRepositoryTest.java (working copy)
@@ -0,0 +1,328 @@
+package org.modeshape.graph.connector.base;
+
+import static org.hamcrest.CoreMatchers.notNullValue;
+import static org.hamcrest.CoreMatchers.nullValue;
+import static org.hamcrest.core.Is.is;
+import static org.junit.Assert.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.UUID;
+import org.junit.Before;
+import org.junit.Test;
+import org.modeshape.graph.ExecutionContext;
+import org.modeshape.graph.Location;
+import org.modeshape.graph.ModeShapeLexicon;
+import org.modeshape.graph.connector.RepositoryContext;
+import org.modeshape.graph.connector.base.MockPathRepository.MockPathTransaction;
+import org.modeshape.graph.property.Name;
+import org.modeshape.graph.property.PathFactory;
+import org.modeshape.graph.property.Property;
+import org.modeshape.graph.property.Path.Segment;
+import org.modeshape.graph.request.CreateWorkspaceRequest.CreateConflictBehavior;
+
+public class PathRepositoryTest {
+
+ private ExecutionContext context;
+ private UUID rootNodeUuid;
+ private String sourceName;
+ private String defaultWorkspaceName;
+ private Repository repository;
+ private BaseRepositorySource source;
+
+ @Before
+ public void beforeEach() throws Exception {
+ context = new ExecutionContext();
+ rootNodeUuid = UUID.randomUUID();
+ sourceName = "Path Source";
+ defaultWorkspaceName = "default";
+
+ RepositoryContext repositoryContext = mock(RepositoryContext.class);
+ when(repositoryContext.getExecutionContext()).thenReturn(context);
+
+ source = mock(BaseRepositorySource.class);
+ when(source.areUpdatesAllowed()).thenReturn(true);
+ when(source.getRootNodeUuidObject()).thenReturn(rootNodeUuid);
+ when(source.getDefaultWorkspaceName()).thenReturn(defaultWorkspaceName);
+ when(source.getName()).thenReturn(sourceName);
+ when(source.getRepositoryContext()).thenReturn(repositoryContext);
+
+ repository = new MockPathRepository(source);
+ }
+
+ private MockPathTransaction beginTransaction() {
+ return (MockPathTransaction)repository.startTransaction(context, false);
+ }
+
+ private MockPathWorkspace defaultWorkspaceFor( MockPathTransaction txn ) {
+ return txn.getWorkspace(defaultWorkspaceName, null);
+ }
+
+ private Name name( String name ) {
+ return context.getValueFactories().getNameFactory().create(name);
+ }
+
+ private Segment segment( String name ) {
+ return context.getValueFactories().getPathFactory().createSegment(name);
+ }
+
+ private PathFactory pathFactory() {
+ return context.getValueFactories().getPathFactory();
+ }
+
+ @Test
+ public void shouldHaveDefaultWorkspace() {
+ assertThat(repository.getWorkspaceNames(), is(Collections.singleton(defaultWorkspaceName)));
+ }
+
+ @Test
+ public void shouldBeAbleToStartTransaction() {
+ assertThat(repository.startTransaction(context, false), is(notNullValue()));
+ }
+
+ @Test
+ public void shouldBeAbleToCreateWorkspaceWithNoClone() {
+ MockPathTransaction txn = beginTransaction();
+
+ String newWorkspaceName = "new workspace";
+ repository.createWorkspace(txn, newWorkspaceName, CreateConflictBehavior.DO_NOT_CREATE, null);
+
+ txn.commit();
+
+ Set workspaceNames = new HashSet(Arrays.asList(new String[] {defaultWorkspaceName, newWorkspaceName}));
+ assertThat(repository.getWorkspaceNames(), is(workspaceNames));
+ }
+
+ @Test
+ public void shouldBeAbleToReadRootNode() {
+ MockPathTransaction txn = beginTransaction();
+ MockPathWorkspace workspace = defaultWorkspaceFor(txn);
+
+ Location location = Location.create(rootNodeUuid);
+
+ PathNode rootNode = txn.getNode(workspace, location);
+ assertThat(rootNode, is(notNullValue()));
+ assertThat(rootNode.getParent(), is(nullValue()));
+ assertThat(rootNode.getUuid(), is(rootNodeUuid));
+ assertThat(rootNode.getName(), is(nullValue()));
+ }
+
+ private Property property( Name name,
+ Object... values ) {
+ return context.getPropertyFactory().create(name, values);
+ }
+
+ @Test
+ public void shouldBeAbleToSetPropertyOnRootNode() {
+ MockPathTransaction txn = beginTransaction();
+ MockPathWorkspace workspace = defaultWorkspaceFor(txn);
+
+ Location location = Location.create(rootNodeUuid);
+ MockPathNode rootNode = txn.getNode(workspace, location);
+
+ Property property = property(ModeShapeLexicon.ROOT, true);
+ rootNode = txn.setProperties(workspace, rootNode, Collections.singleton(property), null, false);
+ assertThat(rootNode.getProperty(ModeShapeLexicon.ROOT), is(notNullValue()));
+ txn.commit();
+
+ txn = beginTransaction();
+ workspace = defaultWorkspaceFor(txn);
+
+ rootNode = txn.getNode(workspace, location);
+ assertThat(rootNode.getProperty(ModeShapeLexicon.ROOT), is(notNullValue()));
+ }
+
+ @Test
+ public void shouldBeAbleToAddChildToRootNode() {
+ MockPathTransaction txn = beginTransaction();
+ MockPathWorkspace workspace = defaultWorkspaceFor(txn);
+
+ Location location = Location.create(rootNodeUuid);
+ MockPathNode rootNode = txn.getNode(workspace, location);
+
+ PathNode newNode = txn.addChild(workspace, rootNode, name("child"), -1, null, null);
+
+ rootNode = txn.getNode(workspace, Location.create(newNode.getParent()));
+ assertThat(rootNode.getChildren().contains(segment("child")), is(true));
+ assertThat(newNode.getParent().isRoot(), is(true));
+ txn.commit();
+
+ txn = beginTransaction();
+ workspace = defaultWorkspaceFor(txn);
+
+ rootNode = txn.getNode(workspace, location);
+ newNode = txn.getNode(workspace, Location.create(pathFactory().create("/child")));
+ assertThat(newNode, is(notNullValue()));
+ assertThat(newNode.getParent(), is(pathFactory().createRootPath()));
+ }
+
+ @Test
+ public void shouldBeAbleToCopyChildToNewParent() {
+ MockPathTransaction txn = beginTransaction();
+ MockPathWorkspace workspace = defaultWorkspaceFor(txn);
+
+ Location location = Location.create(rootNodeUuid);
+ MockPathNode rootNode = txn.getNode(workspace, location);
+
+ /*
+ * Building this graph:
+ * /a
+ * /b
+ * /c
+ * /new
+ */
+
+ MockPathNode aNode = txn.addChild(workspace, rootNode, name("a"), -1, null, null);
+ MockPathNode bNode = txn.addChild(workspace, aNode, name("b"), -1, null, null);
+ MockPathNode cNode = txn.addChild(workspace, bNode, name("c"), -1, null, null);
+
+ MockPathNode newNode = txn.addChild(workspace, rootNode, name("new"), -1, null, null);
+
+ rootNode = txn.getNode(workspace, Location.create(newNode.getParent()));
+ assertThat(rootNode.getChildren().contains(segment("a")), is(true));
+ assertThat(rootNode.getChildren().contains(segment("new")), is(true));
+ assertThat(cNode.getParent(), is(pathFactory().create("/a/b")));
+ assertThat(txn.getNode(workspace, Location.create(pathFactory().create("/a/b/c"))), is(notNullValue()));
+
+ // Have to refresh bNode after the child is added
+ bNode = txn.getNode(workspace, Location.create(pathFactory().create("/a/b")));
+ MockPathNode newBNode = txn.copyNode(workspace, bNode, workspace, newNode, null, true);
+ newNode = txn.getNode(workspace, Location.create(newBNode.getParent()));
+
+ assertThat(rootNode.getChildren().contains(segment("a")), is(true));
+ assertThat(rootNode.getChildren().contains(segment("new")), is(true));
+ assertThat(newNode.getChildren().contains(segment("b")), is(true));
+ assertThat(newBNode.getChildren().contains(segment("c")), is(true));
+
+ txn.commit();
+
+ txn = beginTransaction();
+ workspace = defaultWorkspaceFor(txn);
+
+ rootNode = txn.getNode(workspace, location);
+ newNode = txn.getNode(workspace, Location.create(pathFactory().create("/new")));
+ assertThat(newNode, is(notNullValue()));
+ assertThat(newNode.getParent(), is(pathFactory().createRootPath()));
+ }
+
+ @Test
+ public void shouldBeAbleToMoveChildToNewParent() {
+ MockPathTransaction txn = beginTransaction();
+ MockPathWorkspace workspace = defaultWorkspaceFor(txn);
+
+ Location location = Location.create(rootNodeUuid);
+ MockPathNode rootNode = txn.getNode(workspace, location);
+
+ /*
+ * Building this graph:
+ * /a
+ * /b
+ * /c
+ * /new
+ */
+
+ MockPathNode aNode = txn.addChild(workspace, rootNode, name("a"), -1, null, null);
+ MockPathNode bNode = txn.addChild(workspace, aNode, name("b"), -1, null, null);
+ MockPathNode cNode = txn.addChild(workspace, bNode, name("c"), -1, null, null);
+
+ MockPathNode newNode = txn.addChild(workspace, rootNode, name("new"), -1, null, null);
+
+ rootNode = txn.getNode(workspace, Location.create(newNode.getParent()));
+ assertThat(rootNode.getChildren().contains(segment("a")), is(true));
+ assertThat(rootNode.getChildren().contains(segment("new")), is(true));
+ assertThat(cNode.getParent(), is(pathFactory().create("/a/b")));
+ assertThat(txn.getNode(workspace, Location.create(pathFactory().create("/a/b/c"))), is(notNullValue()));
+
+ txn.commit();
+
+ txn = beginTransaction();
+
+ // Have to refresh bNode after the child is added
+ bNode = txn.getNode(workspace, Location.create(pathFactory().create("/a/b")));
+ Location newBLocation = txn.addChild(workspace, newNode, bNode, null, null);
+ MockPathNode newBNode = txn.getNode(workspace, newBLocation);
+ newNode = txn.getNode(workspace, Location.create(pathFactory().create("/new")));
+
+ cNode = txn.getNode(workspace, Location.create(pathFactory().create("/new/b/c")));
+ assertThat(cNode.getParent(), is(pathFactory().create("/new/b")));
+
+ assertThat(rootNode.getChildren().contains(segment("a")), is(true));
+ assertThat(rootNode.getChildren().contains(segment("new")), is(true));
+ assertThat(newNode.getChildren().contains(segment("b")), is(true));
+ assertThat(newBNode.getChildren().contains(segment("c")), is(true));
+
+ txn.commit();
+
+ txn = beginTransaction();
+ workspace = defaultWorkspaceFor(txn);
+
+ rootNode = txn.getNode(workspace, location);
+ newNode = txn.getNode(workspace, Location.create(pathFactory().create("/new")));
+ assertThat(newNode, is(notNullValue()));
+ assertThat(newNode.getParent(), is(pathFactory().createRootPath()));
+ }
+
+ @Test
+ public void shouldBeAbleToStackMoves() {
+ MockPathTransaction txn = beginTransaction();
+ MockPathWorkspace workspace = defaultWorkspaceFor(txn);
+
+ Location location = Location.create(rootNodeUuid);
+ MockPathNode rootNode = txn.getNode(workspace, location);
+
+ /*
+ * Building this graph:
+ * /a
+ * /b
+ * /c
+ * /new
+ * /d
+ * /e
+ */
+
+ MockPathNode aNode = txn.addChild(workspace, rootNode, name("a"), -1, null, null);
+ MockPathNode bNode = txn.addChild(workspace, aNode, name("b"), -1, null, null);
+ MockPathNode cNode = txn.addChild(workspace, bNode, name("c"), -1, null, null);
+ MockPathNode newNode = txn.addChild(workspace, rootNode, name("new"), -1, null, null);
+ MockPathNode dNode = txn.addChild(workspace, rootNode, name("d"), -1, null, null);
+ MockPathNode eNode = txn.addChild(workspace, dNode, name("e"), -1, null, null);
+
+ txn.commit();
+
+ txn = beginTransaction();
+
+ bNode = txn.getNode(workspace, Location.create(pathFactory().create("/a/b")));
+ Location newBLocation = txn.addChild(workspace, newNode, bNode, null, null);
+
+ MockPathNode newBNode = txn.getNode(workspace, newBLocation);
+ newNode = txn.getNode(workspace, Location.create(pathFactory().create("/new")));
+
+ cNode = txn.getNode(workspace, Location.create(pathFactory().create("/new/b/c")));
+ dNode = txn.getNode(workspace, Location.create(pathFactory().create("/d")));
+ Location newDLocation = txn.addChild(workspace, cNode, dNode, null, null);
+ txn.getNode(workspace, newDLocation);
+
+ eNode = txn.getNode(workspace, Location.create(pathFactory().create("/new/b/c/d/e")));
+ assertThat(eNode.getParent(), is(pathFactory().create("/new/b/c/d")));
+
+
+ rootNode = txn.getNode(workspace, location);
+ assertThat(rootNode.getChildren().contains(segment("new")), is(true));
+ assertThat(newNode.getChildren().contains(segment("b")), is(true));
+ assertThat(newBNode.getChildren().contains(segment("c")), is(true));
+
+ txn.commit();
+
+ txn = beginTransaction();
+ workspace = defaultWorkspaceFor(txn);
+
+ rootNode = txn.getNode(workspace, location);
+ newNode = txn.getNode(workspace, Location.create(pathFactory().create("/new")));
+ assertThat(newNode, is(notNullValue()));
+ assertThat(newNode.getParent(), is(pathFactory().createRootPath()));
+ }
+
+}