Index: modeshape-graph/src/main/java/org/modeshape/graph/connector/RepositorySource.java =================================================================== --- modeshape-graph/src/main/java/org/modeshape/graph/connector/RepositorySource.java (revision 1756) +++ modeshape-graph/src/main/java/org/modeshape/graph/connector/RepositorySource.java (working copy) @@ -28,7 +28,7 @@ import java.math.BigDecimal; import java.net.URI; import java.util.UUID; import javax.naming.Referenceable; -import net.jcip.annotations.NotThreadSafe; +import net.jcip.annotations.ThreadSafe; import org.modeshape.graph.ExecutionContext; import org.modeshape.graph.property.Binary; import org.modeshape.graph.property.DateTime; @@ -44,11 +44,11 @@ import org.modeshape.graph.request.CacheableRequest; *

* Typically this interface is implemented by classes that provide standard-style getters and setters for the various properties * necessary for proper configuration via reflection or introspection. This interface expects nor defines any such properties, - * leaving that entirely to the implementation classes. Although any types can be used for these setters, other ModeShape components use - * reflection to set these properties and work best when the setters accept a single parameter that is a primitive, an array of - * primitives, a value compatible with {@link PropertyType} (e.g., {@link Path}, {@link Name}, {@link URI}, {@link UUID}, - * {@link Reference}, {@link Binary}, {@link Long}, {@link Double}, {@link BigDecimal}, {@link DateTime}, etc.), or an array of - * values that are compatible with {@link PropertyType}. + * leaving that entirely to the implementation classes. Although any types can be used for these setters, other ModeShape + * components use reflection to set these properties and work best when the setters accept a single parameter that is a primitive, + * an array of primitives, a value compatible with {@link PropertyType} (e.g., {@link Path}, {@link Name}, {@link URI}, + * {@link UUID}, {@link Reference}, {@link Binary}, {@link Long}, {@link Double}, {@link BigDecimal}, {@link DateTime}, etc.), or + * an array of values that are compatible with {@link PropertyType}. *

*

* Implementations should also provide a no-arg constructor so that it is possible to easily create instances and initialize using @@ -62,19 +62,19 @@ import org.modeshape.graph.request.CacheableRequest; *

*

Pooling connections

*

- * If the connections created by a RepositorySource are expensive to create, then connection pooling is recommended. ModeShape provides - * this capability with a powerful and flexible {@link RepositoryConnectionPool} class. This is the pooling mechanism used by - * ModeShape, but you are free to use your own pools. + * If the connections created by a RepositorySource are expensive to create, then connection pooling is recommended. ModeShape + * provides this capability with a powerful and flexible {@link RepositoryConnectionPool} class. This is the pooling mechanism + * used by ModeShape, but you are free to use your own pools. *

*

Cache Policy

*

- * Each connector is responsible for determining whether and how long ModeShape is to cache the content made available by the connector. - * This is referred to as the caching policy, and consists of a time to live value representing the number of milliseconds that a - * piece of data may be cached. After the TTL has passed, the information is no longer used. + * Each connector is responsible for determining whether and how long ModeShape is to cache the content made available by the + * connector. This is referred to as the caching policy, and consists of a time to live value representing the number of + * milliseconds that a piece of data may be cached. After the TTL has passed, the information is no longer used. *

*

- * ModeShape allows a connector to use a flexible and powerful caching policy. First, each connection returns the default caching policy - * for all information returned by that connection. Often this policy can be configured via properties on the + * ModeShape allows a connector to use a flexible and powerful caching policy. First, each connection returns the default caching + * policy for all information returned by that connection. Often this policy can be configured via properties on the * {@link RepositorySource} implementation. This is optional, meaning the connector can return null if it does not wish to have a * default caching policy. *

@@ -83,9 +83,9 @@ import org.modeshape.graph.request.CacheableRequest; * this is optional, meaning that a null caching policy on a request implies that the request has no overridden caching policy. *

*

- * Third, if the connector has no default caching policy and none is set on the individual requests, ModeShape uses whatever caching - * policy is set up for that component using the connector. For example, the federating connector allows a default caching policy - * to be specified, and this policy is used should the sources being federated not define their own caching policy. + * Third, if the connector has no default caching policy and none is set on the individual requests, ModeShape uses whatever + * caching policy is set up for that component using the connector. For example, the federating connector allows a default caching + * policy to be specified, and this policy is used should the sources being federated not define their own caching policy. *

*

* In summary, a connector has total control over whether and for how long the information it provides is cached. @@ -135,7 +135,7 @@ import org.modeshape.graph.request.CacheableRequest; * security properties. *

*/ -@NotThreadSafe +@ThreadSafe public interface RepositorySource extends Referenceable, Serializable { /** Index: modeshape-graph/src/main/java/org/modeshape/graph/connector/base/BaseRepositorySource.java new file mode 100644 =================================================================== --- /dev/null (revision 1756) +++ modeshape-graph/src/main/java/org/modeshape/graph/connector/base/BaseRepositorySource.java (working copy) @@ -0,0 +1,66 @@ +/* + * 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 org.modeshape.graph.cache.CachePolicy; +import org.modeshape.graph.connector.RepositoryContext; + +/** + * An extension of the {@link BaseRepositorySource} class that provides a {@link CachePolicy cache policy} and a + * {@link RepositoryContext repository context}. + */ +public interface BaseRepositorySource extends org.modeshape.graph.connector.RepositorySource { + + /** + * Get whether this source allows updates. + * + * @return true if this source allows updates by clients, or false if no updates are allowed + * @see #setUpdatesAllowed(boolean) + */ + boolean areUpdatesAllowed(); + + /** + * Set whether this source allows updates to data within workspaces + * + * @param updatesAllowed true if this source allows updates to data within workspaces clients, or false if updates are not + * allowed. + * @see #areUpdatesAllowed() + */ + void setUpdatesAllowed( boolean updatesAllowed ); + + /** + * Returns the {@link CachePolicy cache policy} for the repository source + * + * @return the {@link CachePolicy cache policy} for the repository source + */ + CachePolicy getDefaultCachePolicy(); + + /** + * Returns the {@link RepositoryContext repository context} for the repository source + * + * @return the {@link RepositoryContext repository context} for the repository source + */ + RepositoryContext getRepositoryContext(); + +} Index: modeshape-graph/src/main/java/org/modeshape/graph/connector/base/BaseTransaction.java new file mode 100644 =================================================================== --- /dev/null (revision 1756) +++ modeshape-graph/src/main/java/org/modeshape/graph/connector/base/BaseTransaction.java (working copy) @@ -0,0 +1,190 @@ +/* + * 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.LinkedList; +import java.util.List; +import java.util.UUID; +import net.jcip.annotations.NotThreadSafe; +import org.modeshape.graph.ExecutionContext; +import org.modeshape.graph.GraphI18n; +import org.modeshape.graph.Location; +import org.modeshape.graph.connector.LockFailedException; +import org.modeshape.graph.property.Name; +import org.modeshape.graph.property.NameFactory; +import org.modeshape.graph.property.Path; +import org.modeshape.graph.property.PathFactory; +import org.modeshape.graph.property.PathNotFoundException; +import org.modeshape.graph.property.PropertyFactory; +import org.modeshape.graph.property.ValueFactories; +import org.modeshape.graph.request.LockBranchRequest.LockScope; + +/** + * @param the type of node + * @param the type of workspace + */ +@NotThreadSafe +public abstract class BaseTransaction + implements Transaction { + + protected final UUID rootNodeUuid; + protected final ExecutionContext context; + protected final PathFactory pathFactory; + protected final NameFactory nameFactory; + protected final PropertyFactory propertyFactory; + protected final ValueFactories valueFactories; + + protected BaseTransaction( ExecutionContext context, + UUID rootNodeUuid ) { + this.rootNodeUuid = rootNodeUuid; + this.context = context; + this.propertyFactory = context.getPropertyFactory(); + this.valueFactories = context.getValueFactories(); + this.pathFactory = valueFactories.getPathFactory(); + this.nameFactory = valueFactories.getNameFactory(); + } + + protected String readable( Object obj ) { + return valueFactories.getStringFactory().create(obj); + } + + /** + * {@inheritDoc} + * + * @see org.modeshape.graph.connector.base.Transaction#getContext() + */ + public ExecutionContext getContext() { + return context; + } + + /** + * {@inheritDoc} + * + * @see org.modeshape.graph.connector.base.Transaction#getRootNode(org.modeshape.graph.connector.base.Workspace) + */ + public NodeType getRootNode( WorkspaceType workspace ) { + return getNode(workspace, Location.create(rootNodeUuid)); + } + + protected NodeType getNode( WorkspaceType workspace, + Path path, + Location location ) { + NodeType node = getRootNode(workspace); + for (Path.Segment segment : path) { + NodeType child = getChild(workspace, node, segment); + if (child == null) { + List segments = new LinkedList(); + for (Path.Segment seg : path) { + if (seg != segment) segments.add(seg); + else break; + } + Path lowestExisting = pathFactory.createAbsolutePath(segments); + throw new PathNotFoundException(location, lowestExisting, GraphI18n.nodeDoesNotExist.text(path)); + } + node = child; + } + return node; + } + + /** + * {@inheritDoc} + * + * @see org.modeshape.graph.connector.base.Transaction#pathFor(org.modeshape.graph.connector.base.Workspace, + * org.modeshape.graph.connector.base.Node) + */ + public Path pathFor( WorkspaceType workspace, + NodeType node ) { + assert node != null; + assert pathFactory != null; + + LinkedList segments = new LinkedList(); + do { + segments.addFirst(node.getName()); + node = getParent(workspace, node); + } while (node != null); + segments.removeFirst(); // remove the root name, which is meaningless + + return pathFactory.createAbsolutePath(segments); + } + + /** + * {@inheritDoc} + *

+ * This method is implemented by iterating through the children, looking for the first child that has a matching name. + * Obviously this may be implemented more efficiently in certain systems. For example, an implementation might create a path + * by appending the child name to the supplied parent, and find the node with this path. + *

+ * + * @see org.modeshape.graph.connector.base.Transaction#getFirstChild(org.modeshape.graph.connector.base.Workspace, + * org.modeshape.graph.connector.base.Node, org.modeshape.graph.property.Name) + */ + public NodeType getFirstChild( WorkspaceType workspace, + NodeType parent, + Name childName ) { + for (NodeType child : getChildren(workspace, parent)) { + if (child.getName().equals(childName)) return child; + } + return null; + } + + /** + * {@inheritDoc} + * + * @see org.modeshape.graph.connector.base.Transaction#lockNode(org.modeshape.graph.connector.base.Workspace, + * org.modeshape.graph.connector.base.Node, org.modeshape.graph.request.LockBranchRequest.LockScope, long) + */ + public void lockNode( WorkspaceType workspace, + NodeType node, + LockScope lockScope, + long lockTimeoutInMillis ) throws LockFailedException { + // do nothing by default + } + + /** + * {@inheritDoc} + * + * @see org.modeshape.graph.connector.base.Transaction#unlockNode(org.modeshape.graph.connector.base.Workspace, + * org.modeshape.graph.connector.base.Node) + */ + public void unlockNode( WorkspaceType workspace, + NodeType node ) { + // do nothing by default + } + + /** + * {@inheritDoc} + * + * @see org.modeshape.graph.connector.base.Transaction#commit() + */ + public void commit() { + } + + /** + * {@inheritDoc} + * + * @see org.modeshape.graph.connector.base.Transaction#rollback() + */ + public void rollback() { + } +} Index: modeshape-graph/src/main/java/org/modeshape/graph/connector/base/Connection.java new file mode 100644 =================================================================== --- /dev/null (revision 1756) +++ modeshape-graph/src/main/java/org/modeshape/graph/connector/base/Connection.java (working copy) @@ -0,0 +1,168 @@ +/* + * 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.concurrent.TimeUnit; +import javax.transaction.xa.XAResource; +import net.jcip.annotations.ThreadSafe; +import org.modeshape.common.statistic.Stopwatch; +import org.modeshape.common.util.Logger; +import org.modeshape.graph.ExecutionContext; +import org.modeshape.graph.cache.CachePolicy; +import org.modeshape.graph.connector.RepositoryConnection; +import org.modeshape.graph.connector.RepositoryContext; +import org.modeshape.graph.connector.RepositorySourceException; +import org.modeshape.graph.observe.Observer; +import org.modeshape.graph.request.Request; +import org.modeshape.graph.request.processor.RequestProcessor; + +/** + * A connection to a {@link Repository}. + * + * @param the node type + * @param the workspace type + */ +@ThreadSafe +public class Connection implements RepositoryConnection { + private final BaseRepositorySource source; + private final Repository repository; + + public Connection( BaseRepositorySource source, + Repository repository ) { + assert source != null; + assert repository != null; + this.source = source; + this.repository = repository; + } + + /** + * {@inheritDoc} + */ + public String getSourceName() { + return source.getName(); + } + + /** + * {@inheritDoc} + */ + public CachePolicy getDefaultCachePolicy() { + return source.getDefaultCachePolicy(); + } + + /** + * {@inheritDoc} + */ + public XAResource getXAResource() { + return null; + } + + /** + * {@inheritDoc} + */ + public boolean ping( long time, + TimeUnit unit ) { + return true; + } + + /** + * {@inheritDoc} + */ + public void close() { + // do nothing + } + + /** + * {@inheritDoc} + * + * @see org.modeshape.graph.connector.RepositoryConnection#execute(org.modeshape.graph.ExecutionContext, + * org.modeshape.graph.request.Request) + */ + public void execute( ExecutionContext context, + Request request ) throws RepositorySourceException { + Logger logger = context.getLogger(getClass()); + Stopwatch sw = null; + if (logger.isTraceEnabled()) { + sw = new Stopwatch(); + sw.start(); + } + // Do any commands update/write? + + boolean commit = true; + Transaction txn = repository.startTransaction(context, request.isReadOnly()); + + RepositoryContext repositoryContext = this.source.getRepositoryContext(); + Observer observer = repositoryContext != null ? repositoryContext.getObserver() : null; + RequestProcessor processor = new Processor(txn, this.repository, observer, + source.areUpdatesAllowed()); + try { + // Obtain the lock and execute the commands ... + processor.process(request); + if (request.hasError() && !request.isReadOnly()) { + // The changes failed, so we need to rollback so we have 'all-or-nothing' behavior + commit = false; + } + } catch (Throwable error) { + commit = false; + } finally { + try { + processor.close(); + } finally { + // Now commit or rollback ... + try { + if (commit) { + txn.commit(); + } else { + // Need to rollback the changes made to the repository ... + txn.rollback(); + } + } catch (Throwable commitOrRollbackError) { + if (commit && !request.hasError() && !request.isFrozen()) { + // Record the error on the request ... + request.setError(commitOrRollbackError); + } + commit = false; // couldn't do it + } + if (commit) { + // Now that we've closed our transaction, we can notify the observer of the committed changes ... + processor.notifyObserverOfChanges(); + } + } + } + if (logger.isTraceEnabled()) { + assert sw != null; + sw.stop(); + logger.trace("MapRepositoryConnection.execute(...) took " + sw.getTotalDuration()); + } + } + + /** + * {@inheritDoc} + * + * @see java.lang.Object#toString() + */ + @Override + public String toString() { + return "Connection to the \"" + getSourceName() + "\" " + repository.getClass().getSimpleName(); + } +} Index: modeshape-graph/src/main/java/org/modeshape/graph/connector/base/MapNode.java new file mode 100644 =================================================================== --- /dev/null (revision 1756) +++ modeshape-graph/src/main/java/org/modeshape/graph/connector/base/MapNode.java (working copy) @@ -0,0 +1,590 @@ +/* + * ModeShape (http://www.modeshape.org) + * See the COPYRIGHT.txt file distributed with this work for information + * regarding copyright ownership. Some portions may be licensed + * to Red Hat, Inc. under one or more contributor license agreements. + * See the AUTHORS.txt file in the distribution for a full listing of + * individual contributors. + * + * ModeShape is free software. Unless otherwise indicated, all code in ModeShape + * is licensed to you under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * ModeShape is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this software; if not, write to the Free + * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA, or see the FSF site: http://www.fsf.org. + */ +package org.modeshape.graph.connector.base; + +import java.io.Serializable; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import org.modeshape.graph.JcrLexicon; +import org.modeshape.graph.ModeShapeLexicon; +import org.modeshape.graph.property.Name; +import org.modeshape.graph.property.Property; +import org.modeshape.graph.property.Path.Segment; + +/** + * A {@link Node} implementation used by the map-based connector (see {@link MapWorkspace} and {@link MapTransaction}), which + * stores all node state in a map. + *

+ * Strictly speaking, this class is not immutable or thread safe. However, the persisted state cannot be changed. Instead, any + * changes made to the object are stored in a transient area and are made "persistable"via the {@link #freeze()} method. + *

+ *

+ * The {@link MapTransaction} maintains an unfrozen, changed instance within it transactional state, and always puts the + * {@link #freeze() frozen}, read-only representation inside the + */ +public class MapNode 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*/Segment name; + private/*final*/UUID parent; + 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 not be null + * @param name the name of the node; may be null only if the parent is also null + * @param parent the UUID of the parent node; may be null only if the name is null + * @param properties the unmodifiable map of properties; may be null or empty + * @param children the unmodificable list of child UUIDs; may be null or empty + */ + public MapNode( UUID uuid, + Segment name, + UUID parent, + Map properties, + List children ) { + this.uuid = uuid; + this.name = name; + this.parent = parent; + this.properties = properties != null ? properties : Collections.emptyMap(); + this.children = children != null ? children : Collections.emptyList(); + assert this.uuid != null; + 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 not be null + * @param name the name of the node; may be null only if the parent is also null + * @param parent the UUID of the parent node; may be null only if the name is null + * @param properties the unmodifiable map of properties; may be null or empty + * @param children the unmodificable list of child UUIDs; may be null or empty + * @param version the version number + */ + protected MapNode( UUID uuid, + Segment name, + UUID parent, + Map properties, + List children, + int version ) { + this.uuid = uuid; + this.name = name; + this.parent = parent; + this.properties = properties != null ? properties : Collections.emptyMap(); + this.children = children != null ? children : Collections.emptyList(); + this.version = version; + assert this.uuid != null; + 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 not be null + * @param name the name of the node; may be null only if the parent is also null + * @param parent the UUID of the parent node; may be null only if the name is null + * @param properties the properties that are to be copied into the new node; may be null or empty + * @param children the unmodificable list of child UUIDs; may be null or empty + */ + public MapNode( UUID uuid, + Segment name, + UUID parent, + Iterable properties, + List children ) { + this.uuid = uuid; + this.name = name; + this.parent = parent; + 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.uuid != null; + 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 MapNode( UUID uuid ) { + this.uuid = uuid; + this.name = null; + this.parent = 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; + } + + /** + * {@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 parent + */ + public UUID getParent() { + return changes != null ? changes.getParent() : parent; + } + + /** + * @return children + */ + public List getChildren() { + return changes != null ? changes.getChildren(false) : children; + } + + /** + * {@inheritDoc} + * + * @see java.lang.Object#hashCode() + */ + @Override + public int hashCode() { + return uuid.hashCode(); + } + + /** + * {@inheritDoc} + * + * @see java.lang.Object#equals(java.lang.Object) + */ + @Override + public boolean equals( Object obj ) { + if (obj == this) return true; + if (obj instanceof Node) { + Node that = (Node)obj; + if (!this.getUuid().equals(that.getUuid())) return false; + return true; + } + return false; + } + + /** + * {@inheritDoc} + * + * @see java.lang.Object#toString() + */ + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + if (this.name == null) { + sb.append("("); + } else { + sb.append(this.name).append(" ("); + } + sb.append(uuid).append(")"); + return sb.toString(); + } + + /** + * {@inheritDoc} + *

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

+ * + * @see java.lang.Object#clone() + */ + @Override + public MapNode clone() { + return new MapNode(uuid, name, parent, properties, 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 MapNode freeze() { + if (!hasChanges()) return this; + return new MapNode(uuid, changes.getName(), changes.getParent(), changes.getUnmodifiableProperties(), + changes.getUnmodifiableChildren(), version + 1); + } + + /** + * Create a copy of this node except using the supplied parent. + * + * @param parent Sets parent to the specified value. + * @return the new map node; never null + */ + public MapNode withParent( UUID parent ) { + if (changes == null) { + MapNode 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 map node; never null + */ + public MapNode withName( Segment name ) { + if (changes == null) { + MapNode 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 UUID of the child that is to be added; may not be null + * @return the new map node; never null + */ + public MapNode withChild( UUID child ) { + assert child != null; + if (changes == null) { + MapNode 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 UUID of the child that is to be added at the end of the existing children + * @return the new map node; never null + */ + public MapNode withChild( int index, + UUID child ) { + assert child != null; + assert index >= 0; + if (changes == null) { + MapNode copy = clone(); + List children = new LinkedList(getChildren()); + assert !children.contains(child); + children.add(index, child); + copy.changes = newChanges(); + copy.changes.setChildren(children); + return copy; + } + changes.getChildren(true).add(index, child); + return this; + } + + /** + * Create a copy of this node except without the supplied child node. + * + * @param child the UUID of the child that is to be removed; may not be null + * @return the new map node; never null + */ + public MapNode withoutChild( UUID child ) { + assert child != null; + if (changes == null) { + MapNode 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 map node; never null + */ + public MapNode withoutChildren() { + if (getChildren().isEmpty()) return this; + if (changes == null) { + MapNode 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 map node; never null + */ + public MapNode withProperties( Iterable propertiesToSet, + Iterable propertiesToRemove, + boolean removeAllExisting ) { + if (propertiesToSet == null && propertiesToRemove == null && !removeAllExisting) { + // no changes ... + return this; + } + Map newProperties = null; + MapNode result = this; + if (changes == null) { + MapNode copy = clone(); + copy.changes = newChanges(); + copy.changes.setProperties(new HashMap(this.properties)); + newProperties = copy.changes.getProperties(true); + result = copy; + } else { + newProperties = changes.getProperties(true); + } + if (removeAllExisting) { + newProperties.clear(); + } else { + if (propertiesToRemove != null) { + for (Name name : propertiesToRemove) { + if (JcrLexicon.UUID.equals(name) || ModeShapeLexicon.UUID.equals(name)) continue; + newProperties.remove(name); + } + } else if (propertiesToSet == null) { + return this; + } + } + if (propertiesToSet != null) { + for (Property property : propertiesToSet) { + newProperties.put(property.getName(), property); + } + } + return result; + } + + /** + * Create a copy of this node except with the new property. + * + * @param property the property to set + * @return this map node + */ + public MapNode withProperty( Property property ) { + if (property == null) return this; + if (changes == null) { + MapNode copy = clone(); + copy.changes = newChanges(); + Map newProps = new HashMap(this.properties); + newProps.put(property.getName(), property); + copy.changes.setProperties(newProps); + return copy; + } + changes.getProperties(true).put(property.getName(), property); + return this; + } + + /** + * Create a copy of this node except with the new property. + * + * @param propertyName the name of the property that is to be removed + * @return this map node, or this node if the named properties does not exist on this node + */ + public MapNode withoutProperty( Name propertyName ) { + if (propertyName == null || !getProperties().containsKey(propertyName)) return this; + if (changes == null) { + MapNode copy = clone(); + copy.changes = newChanges(); + copy.changes.setProperties(new HashMap(this.properties)); + return copy; + } + changes.getProperties(true).remove(propertyName); + return this; + } + + public MapNode withoutProperties() { + if (getProperties().isEmpty()) return this; + if (changes == null) { + MapNode 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 Segment name; + private UUID parent; + private Map properties; + private List children; + + public Segment getName() { + return name != null ? name : MapNode.this.name; + } + + public void setName( Segment name ) { + this.name = name; + } + + public UUID getParent() { + return parent != null ? parent : MapNode.this.parent; + } + + public void setParent( UUID parent ) { + this.parent = parent; + } + + public Map getProperties( boolean createIfMissing ) { + if (properties == null) { + if (createIfMissing) { + properties = new HashMap(MapNode.this.properties); + return properties; + } + return MapNode.this.properties; + } + return properties; + } + + public Map getUnmodifiableProperties() { + return properties != null ? Collections.unmodifiableMap(properties) : MapNode.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 MapNode.this.children; + } + return children; + } + + public List getUnmodifiableChildren() { + return children != null ? Collections.unmodifiableList(children) : MapNode.this.children; + } + + public void setChildren( List children ) { + this.children = children; + } + } + +} Index: modeshape-graph/src/main/java/org/modeshape/graph/connector/base/MapTransaction.java new file mode 100644 =================================================================== --- /dev/null (revision 1756) +++ modeshape-graph/src/main/java/org/modeshape/graph/connector/base/MapTransaction.java (working copy) @@ -0,0 +1,1225 @@ +/* + * 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.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.ListIterator; +import java.util.Map; +import java.util.Set; +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.UuidAlreadyExistsException; +import org.modeshape.graph.property.Name; +import org.modeshape.graph.property.NamespaceRegistry; +import org.modeshape.graph.property.Path; +import org.modeshape.graph.property.PathNotFoundException; +import org.modeshape.graph.property.Property; +import org.modeshape.graph.property.PropertyType; +import org.modeshape.graph.property.Reference; +import org.modeshape.graph.property.UuidFactory; +import org.modeshape.graph.property.ValueFactory; +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 UUID. + * + * @param the type of workspace + * @param the type of node + */ +@NotThreadSafe +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; + + /** + * 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 MapTransaction( Repository repository, + UUID rootNodeUuid ) { + super(repository.getContext(), rootNodeUuid); + this.repository = repository; + } + + /** + * Obtain the repository object against which this transaction is running. + * + * @return the repository object; never null + */ + protected Repository getRepository() { + return repository; + } + + /** + * 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 (uuid != null) { + WorkspaceChanges changes = getChangesFor(workspace, false); + NodeType node = null; + if (changes != null) { + // See if the node we're looking for was deleted ... + if (changes.isRemoved(uuid)) { + // This node was removed within this transaction ... + Path lowestExisting = null; + if (location.hasPath()) { + getNode(workspace, location.getPath(), location); // should fail + assert false; + } + lowestExisting = pathFactory.createRootPath(); + throw new PathNotFoundException(location, lowestExisting, GraphI18n.nodeDoesNotExist.text(readable(location))); + } + // Not deleted, but maybe changed in this transaction ... + node = changes.getChangedOrAdded(uuid); + 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(uuid); + 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 UUID. + * + * @param workspace the workspace; may not be null + * @param uuid the UUID of the node; may not be null + * @return the node, or null if no such node exists + */ + protected NodeType findNode( WorkspaceType workspace, + UUID uuid ) { + WorkspaceChanges changes = getChangesFor(workspace, false); + NodeType node = null; + if (changes != null) { + // See if the node we're looking for was deleted ... + if (changes.isRemoved(uuid)) { + // This node was removed within this transaction ... + return null; + } + // Not deleted, but maybe changed in this transaction ... + node = changes.getChangedOrAdded(uuid); + 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(uuid); + 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 ) { + UUID uuid = node.getUuid(); + changes.removed(uuid); + for (UUID child : node.getChildren()) { + destroyNode(changes, workspace, workspace.getNode(child)); + } + } + + /** + * {@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 ) { + if (uuid == null) { + uuid = UUID.randomUUID(); + } + 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 = findNode(workspace, parent.getUuid()); + } + + 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 ... + newNode = createNode(uuid, pathFactory.createSegment(name, snsIndex), parent.getUuid(), properties); + // And add to the parent ... + parent = (NodeType)parent.withChild(uuid); + } else { + int snsIndex = 0; + ListIterator existingSiblings = getChildren(workspace, parent).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 ... + newNode = createNode(uuid, pathFactory.createSegment(name, snsIndex + 1), parent.getUuid(), properties); + // And add to the parent ... + parent = (NodeType)parent.withChild(index, uuid); + } + 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, parent.getUuid()); + } + // Find the index of the other child ... + int index = parent.getChildren().size(); + if (beforeOtherChild != null) { + if (!beforeOtherChild.getParent().equals(parent.getUuid())) { + // The other child doesn't exist in the parent ... + throw new RepositorySourceException(null); + } + UUID otherChild = beforeOtherChild.getUuid(); + index = parent.getChildren().indexOf(otherChild); + } + + // Find the changes for this workspace ... + WorkspaceChanges changes = getChangesFor(workspace, true); + + // Find the existing parent of the new child, and remove the child ... + NodeType oldParent = getParent(workspace, newChild); + Name newChildName = newChild.getName().getName(); + int snsIndex = newChild.getName().getIndex(); + UUID newChildUuid = newChild.getUuid(); + + if (oldParent != null) { + // Remove the node from it's parent ... + int oldIndex = oldParent.getChildren().indexOf(newChildUuid); + oldParent = (NodeType)oldParent.withoutChild(newChildUuid); + 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); + for (ListIterator iter = siblings.listIterator(oldIndex); iter.hasNext();) { + NodeType sibling = iter.next(); + if (sibling.getName().equals(newChildName)) { + sibling = (NodeType)sibling.withName(pathFactory.createSegment(newChildName, snsIndex++)); + changes.changed(sibling); + } + } + } + + // 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(parent.getUuid()); + parent = (NodeType)parent.withChild(index, newChildUuid); + changes.changed(newChild); + changes.changed(parent); + return Location.create(pathFor(workspace, newChild), newChildUuid); + } + + /** + * Create a new instance of the node, given the supplied UUID. This method should do nothing but instantiate the new node; the + * caller will add to the appropriate maps. + * + * @param uuid the desired UUID; never null + * @param name the name of the new node; may be null if the name is not known + * @param parentUuid the UUID of the parent node; may be null if this is the root node + * @param properties the properties; may be null if there are no properties + * @return the new node; never null + */ + @SuppressWarnings( "unchecked" ) + protected NodeType createNode( UUID uuid, + Segment name, + UUID parentUuid, + Iterable properties ) { + return (NodeType)new MapNode(uuid, name, parentUuid, properties, null); + } + + /** + * {@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) + */ + public NodeType getChild( WorkspaceType workspace, + NodeType parent, + Segment childSegment ) { + List children = new Children(parent.getChildren(), workspace); // don't make a copy + for (NodeType child : children) { + if (child.getName().equals(childSegment)) return child; + } + 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 children = node.getChildren(); // make a copy + if (children.isEmpty()) return Collections.emptyList(); + return new Children(children, workspace); + } + + /** + * {@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 ) { + UUID parentUuid = node.getParent(); + if (parentUuid == null) return null; + return getNode(workspace, Location.create(parentUuid)); + } + + /** + * {@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(createNode(rootNodeUuid, null, null, null)); + return Location.create(pathFactory.createRootPath(), rootNodeUuid); + } + Location result = Location.create(pathFor(workspace, node), node.getUuid()); + + // Find the index of the node in it's parent ... + int index = parent.getChildren().indexOf(node.getUuid()); + 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.getUuid()); + 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 ... + UUID uuid = UUID.randomUUID(); + NodeType copy = addChild(newWorkspace, newParent, desiredName, -1, uuid, original.getProperties().values()); + + Map oldToNewUuids = new HashMap(); + oldToNewUuids.put(original.getUuid(), uuid); + + 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, + false, + oldToNewUuids); + copy = (NodeType)copy.withChild(newChild.getUuid()); + } + } + + // Record the latest changes on the newly-created node .. + WorkspaceChanges changes = getChangesFor(newWorkspace, true); + changes.changed(copy); + + // Now, adjust any references in the new subgraph to objects in the original subgraph + // (because they were internal references, and need to be internal to the new subgraph) + UuidFactory uuidFactory = context.getValueFactories().getUuidFactory(); + ValueFactory referenceFactory = context.getValueFactories().getReferenceFactory(); + for (Map.Entry oldToNew : oldToNewUuids.entrySet()) { + NodeType newNode = findNode(newWorkspace, oldToNew.getValue()); + assert newNode != null; + // Iterate over the properties of the new ... + for (Map.Entry entry : newNode.getProperties().entrySet()) { + Property property = entry.getValue(); + // Now see if any of the property values are references ... + List newValues = new ArrayList(); + boolean foundReference = false; + for (Iterator iter = property.getValues(); iter.hasNext();) { + Object value = iter.next(); + PropertyType type = PropertyType.discoverType(value); + if (type == PropertyType.REFERENCE) { + UUID oldReferencedUuid = uuidFactory.create(value); + UUID newReferencedUuid = oldToNewUuids.get(oldReferencedUuid); + if (newReferencedUuid != null) { + newValues.add(referenceFactory.create(newReferencedUuid)); + foundReference = true; + } + } else { + newValues.add(value); + } + } + // If we found at least one reference, we have to build a new Property object ... + if (foundReference) { + Property newProperty = propertyFactory.create(property.getName(), newValues); + newNode = (NodeType)newNode.withProperty(newProperty); + changes.changed(newNode); + } + } + } + + 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) + */ + @SuppressWarnings( "unchecked" ) + 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 { + + WorkspaceChanges changes = getChangesFor(newWorkspace, true); + NodeType newNode = null; + + // System.out.println("Original workspace under " + pathFor(originalWorkspace, original)); + // print(originalWorkspace, original, 0); + // System.out.println("New workspace under " + pathFor(newWorkspace, newParent)); + // print(newWorkspace, newParent, 0); + Set uuidsInFromBranch = getUuidsUnderNode(originalWorkspace, original); + NodeType existing = null; + if (removeExisting) { + // Remove all of the nodes that have a UUID from under the original node, but DO NOT yet remove + // a node that has the UUID of the original node (as this will be handled later) ... + for (UUID uuid : uuidsInFromBranch) { + if (null != (existing = findNode(newWorkspace, uuid))) { + if (removedExistingNodes != null) { + Path path = pathFor(newWorkspace, existing); + removedExistingNodes.add(Location.create(path, uuid)); + } + removeNode(newWorkspace, existing); + } + } + // System.out.println("New workspace under " + pathFor(newWorkspace, newParent) + " (after removing existing nodes)"); + // print(newWorkspace, newParent, 0); + } else { + uuidsInFromBranch.add(original.getUuid()); // uuidsInFromBranch does not include the UUID of the original + for (UUID uuid : uuidsInFromBranch) { + if (null != (existing = findNode(newWorkspace, uuid))) { + NamespaceRegistry namespaces = context.getNamespaceRegistry(); + String path = pathFor(newWorkspace, existing).getString(namespaces); + throw new UuidAlreadyExistsException(repository.getSourceName(), uuid, path, newWorkspace.getName()); + } + } + } + + UUID uuid = original.getUuid(); + if (desiredSegment != null) { + // Look for an existing child under 'newParent' that has that name ... + int index = 0; + List children = getChildren(newWorkspace, newParent); + NodeType existingWithSameName = null; + for (NodeType child : children) { + if (child.getName().equals(desiredSegment)) { + existingWithSameName = child; + break; + } + ++index; + } + if (index == children.size()) { + // Couldn't find the desired segment, so just append ... + newNode = addChild(newWorkspace, newParent, original.getName().getName(), -1, uuid, original.getProperties() + .values()); + } else { + // Create the new node with the desired name ... + newNode = createNode(uuid, desiredSegment, newParent.getUuid(), original.getProperties().values()); + + // Destroy the existing node ... + assert existingWithSameName != null; + destroyNode(newWorkspace, existingWithSameName); + + // Replace the existing node ... + newParent = (NodeType)newParent.withoutChild(existingWithSameName.getUuid()).withChild(index, uuid); + } + } else { + // Need to remove the existing node with the same UUID ... + existing = findNode(newWorkspace, original.getUuid()); + if (existing != null) { + if (removedExistingNodes != null) { + Path path = pathFor(newWorkspace, existing); + removedExistingNodes.add(Location.create(path, original.getUuid())); + } + removeNode(newWorkspace, existing); + } + + if (desiredName != null) { + // Simply add a new node with the desired name ... + newNode = addChild(newWorkspace, newParent, desiredName, -1, uuid, original.getProperties().values()); + } else { + // Simply append and use the original's name ... + newNode = addChild(newWorkspace, newParent, original.getName().getName(), -1, uuid, original.getProperties() + .values()); + } + } + + // If the parent doesn't already have changes, we need to find the new parent in the newWorkspace's changes + if (!newParent.hasChanges()) { + newParent = findNode(newWorkspace, newParent.getUuid()); + } + + // Walk through the original branch in its workspace ... + for (NodeType originalChild : getChildren(originalWorkspace, original)) { + NodeType newChild = copyBranch(originalWorkspace, originalChild, changes, newWorkspace, newNode, true, null); + newNode = (NodeType)newNode.withChild(newChild.getUuid()); + } + changes.changed(newNode); + + return newNode; + } + + /** + * Returns all of the UUIDs in the branch rooted at {@code node}. The UUID of {@code node} will be included in the set + * of returned UUIDs. + * + * @param workspace the workspace + * @param node the root of the branch + * @return all of the UUIDs in the branch rooted at {@code node} + */ + protected Set getUuidsUnderNode( WorkspaceType workspace, + NodeType node ) { + Set uuids = new HashSet(); + uuidsUnderNode(workspace, node, uuids); + return uuids; + } + + private void uuidsUnderNode( WorkspaceType workspace, + NodeType node, + Set accumulator ) { + for (NodeType child : getChildren(workspace, node)) { + accumulator.add(child.getUuid()); + uuidsUnderNode(workspace, child, accumulator); + } + } + + @SuppressWarnings( "unchecked" ) + protected NodeType copyBranch( WorkspaceType originalWorkspace, + NodeType original, + WorkspaceChanges newWorkspaceChanges, + WorkspaceType newWorkspace, + NodeType newParent, + boolean reuseUuid, + Map oldToNewUuids ) { + // Create the new node (or reuse the original if we can) ... + UUID copyUuid = reuseUuid ? original.getUuid() : UUID.randomUUID(); + NodeType copy = createNode(copyUuid, original.getName(), newParent.getUuid(), original.getProperties().values()); + newWorkspaceChanges.created(copy); + if (!reuseUuid) { + assert oldToNewUuids != null; + oldToNewUuids.put(original.getUuid(), copy.getUuid()); + } + + // Walk through the children and call this method recursively ... + for (NodeType originalChild : getChildren(originalWorkspace, original)) { + NodeType newChild = copyBranch(originalWorkspace, + originalChild, + newWorkspaceChanges, + newWorkspace, + copy, + reuseUuid, + oldToNewUuids); + copy = (NodeType)copy.withChild(newChild.getUuid()); + } + newWorkspaceChanges.changed(copy); + return copy; + } + + /** + * {@inheritDoc} + *

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

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

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

+ * + * @see org.modeshape.graph.connector.base.Transaction#search(org.modeshape.graph.connector.base.Workspace, + * org.modeshape.graph.request.FullTextSearchRequest) + */ + public QueryResults search( WorkspaceType workspace, + FullTextSearchRequest search ) { + return null; + } + + /** + * {@inheritDoc} + * + * @see org.modeshape.graph.connector.base.BaseTransaction#commit() + */ + @Override + public void commit() { + super.commit(); + // Push all of the changes or added nodes onto the workspace ... + if (changesByWorkspaceName != null) { + for (WorkspaceChanges changes : changesByWorkspaceName.values()) { + changes.commit(); + } + changesByWorkspaceName.clear(); + } + } + + /** + * {@inheritDoc} + * + * @see org.modeshape.graph.connector.base.BaseTransaction#rollback() + */ + @Override + public void rollback() { + super.rollback(); + if (changesByWorkspaceName != null) { + changesByWorkspaceName.clear(); + } + } + + /** + * Record of the changes made to a particular workspace. + */ + protected class WorkspaceChanges { + private final WorkspaceType workspace; + private final Map changedOrAddedNodes = new HashMap(); + private final Set removedNodes = new HashSet(); + private boolean removeAll = false; + + protected WorkspaceChanges( WorkspaceType workspace ) { + this.workspace = workspace; + } + + public WorkspaceType getWorkspace() { + return workspace; + } + + public void removeAll( NodeType newRootNode ) { + changedOrAddedNodes.clear(); + removedNodes.clear(); + removeAll = true; + changedOrAddedNodes.put(newRootNode.getUuid(), newRootNode); + } + + public boolean isRemoved( UUID uuid ) { + return removedNodes.contains(uuid); + } + + public NodeType getChangedOrAdded( UUID uuid ) { + return changedOrAddedNodes.get(uuid); + } + + public void removed( UUID uuid ) { + removedNodes.add(uuid); + changedOrAddedNodes.remove(uuid); + } + + public void created( NodeType node ) { + UUID uuid = node.getUuid(); + removedNodes.remove(uuid); + changedOrAddedNodes.put(uuid, node); + } + + public void changed( NodeType node ) { + UUID uuid = node.getUuid(); + assert !removedNodes.contains(uuid); // should not be removed + changedOrAddedNodes.put(uuid, node); + } + + @SuppressWarnings( "unchecked" ) + public void commit() { + if (removeAll) { + workspace.removeAll(); + } + for (NodeType changed : changedOrAddedNodes.values()) { + workspace.putNode((NodeType)changed.freeze()); + } + for (UUID uuid : removedNodes) { + workspace.removeNode(uuid); + } + } + } + + protected class Children implements List { + private final List uuids; + private final WorkspaceType workspace; + private final List cache; + + protected Children( List uuids, + WorkspaceType workspace ) { + this.uuids = uuids; + this.workspace = workspace; + this.cache = new ArrayList(uuids.size()); + } + + /** + * {@inheritDoc} + * + * @see java.util.List#size() + */ + public int size() { + return uuids.size(); + } + + /** + * {@inheritDoc} + * + * @see java.util.List#contains(java.lang.Object) + */ + public boolean contains( Object o ) { + if (o instanceof MapNode) { + return uuids.contains(((MapNode)o).getUuid()); + } + return false; + } + + /** + * {@inheritDoc} + * + * @see java.util.List#containsAll(java.util.Collection) + */ + public boolean containsAll( Collection c ) { + for (Object o : c) { + if (!contains(o)) return false; + } + return true; + } + + /** + * {@inheritDoc} + * + * @see java.util.List#get(int) + */ + public NodeType get( int index ) { + NodeType result = null; + if (cache.size() > index) { + result = cache.get(index); + } + if (result == null) { + UUID uuid = uuids.get(index); + // Do the fast lookup first, but this returns a null if not found ... + result = findNode(workspace, uuid); + if (result == null) { + // Wasn't found, so we need to throw an exception ... + result = getNode(workspace, Location.create(uuid)); + } + while (cache.size() <= index) { + cache.add(null); + } + cache.set(index, result); + } + return result; + } + + /** + * {@inheritDoc} + * + * @see java.util.List#indexOf(java.lang.Object) + */ + public int indexOf( Object o ) { + if (o instanceof MapNode) { + return uuids.indexOf(((MapNode)o).getUuid()); + } + return -1; + } + + /** + * {@inheritDoc} + * + * @see java.util.List#lastIndexOf(java.lang.Object) + */ + public int lastIndexOf( Object o ) { + // The list of UUIDs cannot contain duplicates, so just do the simple thing here ... + return indexOf(0); + } + + /** + * {@inheritDoc} + * + * @see java.util.List#isEmpty() + */ + public boolean isEmpty() { + return uuids.isEmpty(); + } + + /** + * {@inheritDoc} + * + * @see java.util.List#iterator() + */ + public Iterator iterator() { + return listIterator(0); + } + + /** + * {@inheritDoc} + * + * @see java.util.List#listIterator() + */ + public ListIterator listIterator() { + return listIterator(0); + } + + /** + * {@inheritDoc} + * + * @see java.util.List#listIterator(int) + */ + public ListIterator listIterator( final int index ) { + final int maxIndex = size(); + return new ListIterator() { + private int current = index; + + public boolean hasNext() { + return current < maxIndex; + } + + public NodeType next() { + return get(current++); + } + + public void remove() { + throw new UnsupportedOperationException(); + } + + public void add( NodeType e ) { + throw new UnsupportedOperationException(); + } + + public boolean hasPrevious() { + return current > 0; + } + + public int nextIndex() { + return current; + } + + public NodeType previous() { + return get(--current); + } + + public int previousIndex() { + return current - 1; + } + + public void set( NodeType e ) { + throw new UnsupportedOperationException(); + } + }; + } + + /** + * {@inheritDoc} + * + * @see java.util.List#subList(int, int) + */ + public List subList( int fromIndex, + int toIndex ) { + return new Children(uuids.subList(fromIndex, toIndex), workspace); + } + + /** + * {@inheritDoc} + * + * @see java.util.List#toArray() + */ + public Object[] toArray() { + final int length = uuids.size(); + Object[] result = new Object[length]; + for (int i = 0; i != length; ++i) { + result[i] = get(i); + } + return result; + } + + /** + * {@inheritDoc} + * + * @see java.util.List#toArray(T[]) + */ + @SuppressWarnings( "unchecked" ) + public T[] toArray( T[] a ) { + final int length = uuids.size(); + for (int i = 0; i != length; ++i) { + a[i] = (T)get(i); + } + return a; + } + + /** + * {@inheritDoc} + * + * @see java.util.List#add(java.lang.Object) + */ + public boolean add( NodeType e ) { + throw new UnsupportedOperationException(); + } + + /** + * {@inheritDoc} + * + * @see java.util.List#add(int, java.lang.Object) + */ + public void add( int index, + NodeType element ) { + throw new UnsupportedOperationException(); + } + + /** + * {@inheritDoc} + * + * @see java.util.List#addAll(java.util.Collection) + */ + public boolean addAll( Collection c ) { + throw new UnsupportedOperationException(); + } + + /** + * {@inheritDoc} + * + * @see java.util.List#addAll(int, java.util.Collection) + */ + public boolean addAll( int index, + Collection c ) { + throw new UnsupportedOperationException(); + } + + /** + * {@inheritDoc} + * + * @see java.util.List#clear() + */ + public void clear() { + throw new UnsupportedOperationException(); + } + + /** + * {@inheritDoc} + * + * @see java.util.List#remove(java.lang.Object) + */ + public boolean remove( Object o ) { + throw new UnsupportedOperationException(); + } + + /** + * {@inheritDoc} + * + * @see java.util.List#remove(int) + */ + public NodeType remove( int index ) { + throw new UnsupportedOperationException(); + } + + /** + * {@inheritDoc} + * + * @see java.util.List#removeAll(java.util.Collection) + */ + public boolean removeAll( Collection c ) { + throw new UnsupportedOperationException(); + } + + /** + * {@inheritDoc} + * + * @see java.util.List#retainAll(java.util.Collection) + */ + public boolean retainAll( Collection c ) { + throw new UnsupportedOperationException(); + } + + /** + * {@inheritDoc} + * + * @see java.util.List#set(int, java.lang.Object) + */ + public NodeType set( int index, + NodeType element ) { + throw new UnsupportedOperationException(); + } + } +} Index: modeshape-graph/src/main/java/org/modeshape/graph/connector/base/MapWorkspace.java new file mode 100644 =================================================================== --- /dev/null (revision 1756) +++ modeshape-graph/src/main/java/org/modeshape/graph/connector/base/MapWorkspace.java (working copy) @@ -0,0 +1,147 @@ +/* + * ModeShape (http://www.modeshape.org) + * See the COPYRIGHT.txt file distributed with this work for information + * regarding copyright ownership. Some portions may be licensed + * to Red Hat, Inc. under one or more contributor license agreements. + * See the AUTHORS.txt file in the distribution for a full listing of + * individual contributors. + * + * ModeShape is free software. Unless otherwise indicated, all code in ModeShape + * is licensed to you under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * ModeShape is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this software; if not, write to the Free + * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA, or see the FSF site: http://www.fsf.org. + */ +package org.modeshape.graph.connector.base; + +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.locks.ReadWriteLock; +import net.jcip.annotations.NotThreadSafe; + +/** + * The {@link Workspace} implementation that represents all nodes as {@link MapNode} objects and stores them within a {@link Map} + * keyed by their UUID. + *

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

+ * + * @param the type of node + */ +@NotThreadSafe +public class MapWorkspace implements Workspace { + + private final String name; + private final UUID rootNodeUuid; + private final Map nodesByUuid; + + /** + * Create a new instance of the workspace. + * + * @param name the workspace name; may not be null + * @param nodesByUuid the map of nodes keyed by their UUIDs; may not be null + * @param rootNode the root node that is expected to already exist in the map + */ + public MapWorkspace( String name, + Map nodesByUuid, + NodeType rootNode ) { + this.name = name; + this.rootNodeUuid = rootNode.getUuid(); + this.nodesByUuid = nodesByUuid; + this.nodesByUuid.put(rootNodeUuid, rootNode); + assert this.name != null; + assert this.rootNodeUuid != null; + assert this.nodesByUuid != null; + } + + /** + * Create a new instance of the workspace. + * + * @param name the workspace name; may not be null + * @param nodesByUuid the map of nodes keyed by their UUIDs; may not be null + * @param originalToClone the workspace that is to be cloned; may not be null + */ + public MapWorkspace( String name, + Map nodesByUuid, + MapWorkspace originalToClone ) { + this.name = name; + this.nodesByUuid = nodesByUuid; + this.nodesByUuid.putAll(originalToClone.nodesByUuid); // make a copy + this.rootNodeUuid = originalToClone.getRootNode().getUuid(); + assert this.name != null; + assert this.rootNodeUuid != null; + assert this.nodesByUuid != null; + } + + /** + * {@inheritDoc} + * + * @see org.modeshape.graph.connector.base.Workspace#getName() + */ + public String getName() { + return name; + } + + /** + * Get the root node in this workspace. + * + * @return the root node; never null + */ + public NodeType getRootNode() { + return nodesByUuid.get(rootNodeUuid); + } + + /** + * Get the node with the supplied UUID. + * + * @param uuid the UUID of the node + * @return the node state as known by this workspace, or null if no such node exists in this workspace + */ + public NodeType getNode( UUID uuid ) { + return nodesByUuid.get(uuid); + } + + /** + * Add the node into this workspace's map, overwriting any previous record of the node + * + * @param node the new node; may not be null + * @return the previous node state, or null if the node is new to this workspace + */ + public NodeType putNode( NodeType node ) { + return nodesByUuid.put(node.getUuid(), node); + } + + /** + * Remove and return the node with the supplied UUID. This method will never remove the root node. + * + * @param uuid the UUID of the node to be removed + * @return the node that was removed, or null if the supplied UUID is the root node's UUID or if this workspace does not + * contain a node with the supplied UUID + */ + public NodeType removeNode( UUID uuid ) { + return rootNodeUuid.equals(uuid) ? null : nodesByUuid.remove(uuid); + } + + /** + * Remove all of the nodes in this workspace, and make sure there is a single root node with no properties and no children. + */ + @SuppressWarnings( "unchecked" ) + public void removeAll() { + NodeType newRootNode = (NodeType)getRootNode().withoutChildren().withoutProperties().freeze(); + nodesByUuid.clear(); + nodesByUuid.put(newRootNode.getUuid(), newRootNode); + } +} Index: modeshape-graph/src/main/java/org/modeshape/graph/connector/base/Node.java new file mode 100644 =================================================================== --- /dev/null (revision 1756) +++ modeshape-graph/src/main/java/org/modeshape/graph/connector/base/Node.java (working copy) @@ -0,0 +1,68 @@ +/* + * ModeShape (http://www.modeshape.org) + * See the COPYRIGHT.txt file distributed with this work for information + * regarding copyright ownership. Some portions may be licensed + * to Red Hat, Inc. under one or more contributor license agreements. + * See the AUTHORS.txt file in the distribution for a full listing of + * individual contributors. + * + * ModeShape is free software. Unless otherwise indicated, all code in ModeShape + * is licensed to you under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * ModeShape is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this software; if not, write to the Free + * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA, or see the FSF site: http://www.fsf.org. + */ +package org.modeshape.graph.connector.base; + +import java.util.Map; +import java.util.UUID; +import net.jcip.annotations.Immutable; +import org.modeshape.graph.property.Name; +import org.modeshape.graph.property.Path; +import org.modeshape.graph.property.Property; + +/** + * A snapshot of a single node within a map-based repository. + */ +@Immutable +public interface Node { + + /** + * Returns the UUID for this node + * + * @return the UUID for this node + */ + UUID getUuid(); + + /** + * Returns the name of this node along with its SNS index within its parent's children + * + * @return the name of this node along with its SNS index within its parent's children, or null for the root node + */ + Path.Segment getName(); + + /** + * Returns properties of this node. + * + * @return an immutable map of properties keyed by their name. + */ + Map getProperties(); + + /** + * Returns the property with the supplied name. + * + * @param name the name of the property + * @return the property, or null if this node has no such property + */ + Property getProperty( Name name ); + +} Index: modeshape-graph/src/main/java/org/modeshape/graph/connector/base/Processor.java new file mode 100644 =================================================================== --- /dev/null (revision 1756) +++ modeshape-graph/src/main/java/org/modeshape/graph/connector/base/Processor.java (working copy) @@ -0,0 +1,602 @@ +/* + * 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.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import org.modeshape.graph.GraphI18n; +import org.modeshape.graph.JcrLexicon; +import org.modeshape.graph.Location; +import org.modeshape.graph.ModeShapeLexicon; +import org.modeshape.graph.observe.Observer; +import org.modeshape.graph.property.Name; +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.query.QueryResults; +import org.modeshape.graph.request.AccessQueryRequest; +import org.modeshape.graph.request.CloneBranchRequest; +import org.modeshape.graph.request.CloneWorkspaceRequest; +import org.modeshape.graph.request.CopyBranchRequest; +import org.modeshape.graph.request.CreateNodeRequest; +import org.modeshape.graph.request.CreateWorkspaceRequest; +import org.modeshape.graph.request.DeleteBranchRequest; +import org.modeshape.graph.request.DestroyWorkspaceRequest; +import org.modeshape.graph.request.FullTextSearchRequest; +import org.modeshape.graph.request.GetWorkspacesRequest; +import org.modeshape.graph.request.InvalidRequestException; +import org.modeshape.graph.request.InvalidWorkspaceException; +import org.modeshape.graph.request.LockBranchRequest; +import org.modeshape.graph.request.MoveBranchRequest; +import org.modeshape.graph.request.ReadAllChildrenRequest; +import org.modeshape.graph.request.ReadAllPropertiesRequest; +import org.modeshape.graph.request.Request; +import org.modeshape.graph.request.UnlockBranchRequest; +import org.modeshape.graph.request.UpdatePropertiesRequest; +import org.modeshape.graph.request.VerifyWorkspaceRequest; +import org.modeshape.graph.request.processor.RequestProcessor; + +/** + * The default implementation of the {@link RequestProcessor} for map repositories. + * + * @param the node type + * @param the workspace type + */ +public class Processor extends RequestProcessor { + private final PathFactory pathFactory; + private final PropertyFactory propertyFactory; + private final Repository repository; + private final boolean updatesAllowed; + private final Transaction txn; + + public Processor( Transaction txn, + Repository repository, + Observer observer, + boolean updatesAllowed ) { + super(repository.getSourceName(), txn.getContext(), observer); + this.txn = txn; + this.repository = repository; + this.pathFactory = txn.getContext().getValueFactories().getPathFactory(); + this.propertyFactory = txn.getContext().getPropertyFactory(); + this.updatesAllowed = updatesAllowed; + } + + /** + * {@inheritDoc} + * + * @see org.modeshape.graph.request.processor.RequestProcessor#process(org.modeshape.graph.request.ReadAllChildrenRequest) + */ + @Override + public void process( ReadAllChildrenRequest request ) { + WorkspaceType workspace = getWorkspace(request, request.inWorkspace()); + NodeType node = getTargetNode(workspace, request, request.of()); + if (node == null) { + assert request.hasError(); + return; + } + + Location actualLocation = getActualLocation(workspace, request.of(), node); + assert actualLocation != null; + Path path = actualLocation.getPath(); + // Get the names of the children ... + List children = txn.getChildren(workspace, node); + for (Node child : children) { + Segment childName = child.getName(); + Path childPath = pathFactory.create(path, childName); + request.addChild(childPath, propertyFactory.create(ModeShapeLexicon.UUID, child.getUuid())); + } + request.setActualLocationOfNode(actualLocation); + setCacheableInfo(request); + } + + @Override + public void process( LockBranchRequest request ) { + WorkspaceType workspace = getWorkspace(request, request.inWorkspace()); + NodeType node = getTargetNode(workspace, request, request.at()); + if (node == null) return; + + txn.lockNode(workspace, node, request.lockScope(), request.lockTimeoutInMillis()); + + Location actualLocation = getActualLocation(workspace, request.at(), node); + request.setActualLocation(actualLocation); + recordChange(request); + } + + @Override + public void process( UnlockBranchRequest request ) { + WorkspaceType workspace = getWorkspace(request, request.inWorkspace()); + NodeType node = getTargetNode(workspace, request, request.at()); + if (node == null) return; + + txn.unlockNode(workspace, node); + + Location actualLocation = getActualLocation(workspace, request.at(), node); + request.setActualLocation(actualLocation); + recordChange(request); + } + + @Override + public void process( ReadAllPropertiesRequest request ) { + WorkspaceType workspace = getWorkspace(request, request.inWorkspace()); + NodeType node = getTargetNode(workspace, request, request.at()); + if (node == null) { + assert request.hasError(); + return; + } + + // Get the properties of the node ... + Location actualLocation = getActualLocation(workspace, request.at(), node); + request.addProperty(propertyFactory.create(ModeShapeLexicon.UUID, node.getUuid())); + request.addProperties(node.getProperties().values()); + + assert actualLocation != null; + request.setActualLocationOfNode(actualLocation); + setCacheableInfo(request); + } + + /** + * {@inheritDoc} + * + * @see org.modeshape.graph.request.processor.RequestProcessor#process(org.modeshape.graph.request.CloneBranchRequest) + */ + @Override + public void process( CloneBranchRequest request ) { + if (!updatesAllowed(request)) return; + + WorkspaceType workspace = getWorkspace(request, request.fromWorkspace()); + WorkspaceType newWorkspace = getWorkspace(request, request.intoWorkspace()); + if (workspace == null || newWorkspace == null) return; + NodeType node = getTargetNode(workspace, request, request.from()); + if (node == null) return; + + // Look up the new parent, which must exist ... + Path newParentPath = request.into().getPath(); + NodeType newParent = txn.getNode(newWorkspace, request.into()); + Set removedExistingNodes = new HashSet(); + NodeType newNode = txn.cloneNode(workspace, + node, + newWorkspace, + newParent, + request.desiredName(), + request.desiredSegment(), + request.removeExisting(), + removedExistingNodes); + Path newPath = getExecutionContext().getValueFactories().getPathFactory().create(newParentPath, newNode.getName()); + Location oldLocation = getActualLocation(workspace, request.from(), node); + Location newLocation = Location.create(newPath, newNode.getUuid()); + request.setActualLocations(oldLocation, newLocation); + request.setRemovedNodes(Collections.unmodifiableSet(removedExistingNodes)); + recordChange(request); + } + + /** + * {@inheritDoc} + * + * @see org.modeshape.graph.request.processor.RequestProcessor#process(org.modeshape.graph.request.CopyBranchRequest) + */ + @Override + public void process( CopyBranchRequest request ) { + if (!updatesAllowed(request)) return; + + WorkspaceType workspace = getWorkspace(request, request.fromWorkspace()); + WorkspaceType newWorkspace = getWorkspace(request, request.intoWorkspace()); + if (workspace == null || newWorkspace == null) return; + NodeType node = getTargetNode(workspace, request, request.from()); + if (node == null) return; + + // Look up the new parent, which must exist ... + Path newParentPath = request.into().getPath(); + Name desiredName = request.desiredName(); + NodeType newParent = getTargetNode(newWorkspace, request, request.into()); + if (newParent == null) return; + NodeType newNode = txn.copyNode(workspace, node, newWorkspace, newParent, desiredName, true); + Path newPath = getExecutionContext().getValueFactories().getPathFactory().create(newParentPath, newNode.getName()); + Location oldLocation = getActualLocation(workspace, request.from(), node); + Location newLocation = Location.create(newPath, newNode.getUuid()); + request.setActualLocations(oldLocation, newLocation); + recordChange(request); + } + + /** + * {@inheritDoc} + * + * @see org.modeshape.graph.request.processor.RequestProcessor#process(org.modeshape.graph.request.CreateNodeRequest) + */ + @Override + public void process( CreateNodeRequest request ) { + if (!updatesAllowed(request)) return; + + WorkspaceType workspace = getWorkspace(request, request.inWorkspace()); + if (workspace == null) return; + + // Look up the parent node, which must exist ... + NodeType parentNode = txn.getNode(workspace, request.under()); + + UUID uuid = null; + // Make a list of the properties that we will store: all props except dna:uuid and jcr:uuid + List propsToStore = new ArrayList(request.properties().size()); + for (Property property : request.properties()) { + if (property.getName().equals(ModeShapeLexicon.UUID) || property.getName().equals(JcrLexicon.UUID)) { + uuid = getExecutionContext().getValueFactories().getUuidFactory().create(property.getValues().next()); + } else { + if (property.size() > 0) propsToStore.add(property); + } + } + + NodeType node = null; + switch (request.conflictBehavior()) { + case APPEND: + node = txn.addChild(workspace, parentNode, request.named(), -1, uuid, propsToStore); + break; + case DO_NOT_REPLACE: + node = txn.getFirstChild(workspace, parentNode, request.named()); + if (node == null) { + node = txn.addChild(workspace, parentNode, request.named(), -1, uuid, propsToStore); + } + break; + case REPLACE: + // 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); + } else { + node = txn.addChild(workspace, parentNode, request.named(), -1, uuid, propsToStore); + } + break; + case UPDATE: + // 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) { + node = txn.addChild(workspace, parentNode, request.named(), -1, uuid, propsToStore); + } else { + // otherwise, we found it and we need to replace the properties ... + txn.setProperties(workspace, node, propsToStore, null, true); + // and remove the children ... + txn.removeAllChildren(workspace, node); + } + break; + } + assert node != null; + Path path = txn.pathFor(workspace, node); + Location actualLocation = getActualLocation(workspace, Location.create(path), node); + request.setActualLocationOfNode(actualLocation); + recordChange(request); + } + + /** + * {@inheritDoc} + * + * @see org.modeshape.graph.request.processor.RequestProcessor#process(org.modeshape.graph.request.DeleteBranchRequest) + */ + @Override + public void process( DeleteBranchRequest request ) { + if (!updatesAllowed(request)) return; + + WorkspaceType workspace = getWorkspace(request, request.inWorkspace()); + if (workspace == null) return; + NodeType node = getTargetNode(workspace, request, request.at()); + if (node == null) return; + Location actualLocation = txn.removeNode(workspace, node); + request.setActualLocationOfNode(actualLocation); + recordChange(request); + } + + /** + * {@inheritDoc} + * + * @see org.modeshape.graph.request.processor.RequestProcessor#process(org.modeshape.graph.request.MoveBranchRequest) + */ + @Override + public void process( MoveBranchRequest request ) { + if (!updatesAllowed(request)) return; + + WorkspaceType workspace = getWorkspace(request, request.inWorkspace()); + if (workspace == null) return; + + NodeType beforeNode = request.before() != null ? getTargetNode(workspace, request, request.before()) : null; + NodeType node = getTargetNode(workspace, request, request.from()); + if (node == null) return; + if (request.hasError()) return; // if beforeNode could not be found + // Look up the new parent, which must exist ... + + NodeType newParent = null; + Location newLocation = null; + if (request.into() != null) { + newParent = txn.getNode(workspace, request.into()); // this will fail if there is no node + newLocation = txn.addChild(workspace, newParent, node, null, request.desiredName()); + } else { + // into or before cannot both be null + assert beforeNode != null; + // The new parent is the parent of the before node ... + newParent = txn.getParent(workspace, beforeNode); + if (newParent == null) { + // The before node must be the root ... + request.setError(new PathNotFoundException(request.into(), pathFactory.createRootPath(), + GraphI18n.nodeDoesNotExist.text("parent of root"))); + return; + } + // Move the node into the parent of the beforeNode ... + newLocation = txn.addChild(workspace, newParent, node, beforeNode, request.desiredName()); + } + assert newParent != null; + + Location oldLocation = getActualLocation(workspace, request.from(), node); + request.setActualLocations(oldLocation, newLocation); + recordChange(request); + } + + /** + * {@inheritDoc} + * + * @see org.modeshape.graph.request.processor.RequestProcessor#process(org.modeshape.graph.request.UpdatePropertiesRequest) + */ + @Override + public void process( UpdatePropertiesRequest request ) { + if (!updatesAllowed(request)) return; + + WorkspaceType workspace = getWorkspace(request, request.inWorkspace()); + NodeType node = getTargetNode(workspace, request, request.on()); + if (node == null) return; + // Figure out which properties should be set ... + List propertiesToSet = null; + Set propertiesToRemove = null; + for (Map.Entry propertyEntry : request.properties().entrySet()) { + Property property = propertyEntry.getValue(); + if (property == null) { + if (propertiesToRemove == null) propertiesToRemove = new HashSet(); + propertiesToRemove.add(propertyEntry.getKey()); + continue; + } + Name propName = property.getName(); + if (!propName.equals(ModeShapeLexicon.UUID)) { + if (node.getProperties().get(propName) == null) { + // It is a new property ... + request.setNewProperty(propName); + } + if (propertiesToSet == null) propertiesToSet = new LinkedList(); + propertiesToSet.add(property); + } + } + + txn.setProperties(workspace, node, propertiesToSet, propertiesToRemove, request.removeOtherProperties()); + Location actualLocation = getActualLocation(workspace, request.on(), node); + request.setActualLocationOfNode(actualLocation); + recordChange(request); + } + + /** + * {@inheritDoc} + * + * @see org.modeshape.graph.request.processor.RequestProcessor#process(org.modeshape.graph.request.CreateWorkspaceRequest) + */ + @Override + public void process( CreateWorkspaceRequest request ) { + if (!updatesAllowed(request)) return; + + String clonedWorkspaceName = null; + WorkspaceType workspace = repository.createWorkspace(txn, + request.desiredNameOfNewWorkspace(), + request.conflictBehavior(), + clonedWorkspaceName); + if (workspace == null) { + String msg = GraphI18n.workspaceAlreadyExistsInRepository.text(request.desiredNameOfNewWorkspace(), + repository.getSourceName()); + request.setError(new InvalidWorkspaceException(msg)); + } else { + Node root = txn.getRootNode(workspace); + request.setActualRootLocation(Location.create(pathFactory.createRootPath(), root.getUuid())); + request.setActualWorkspaceName(workspace.getName()); + recordChange(request); + } + } + + /** + * {@inheritDoc} + * + * @see org.modeshape.graph.request.processor.RequestProcessor#process(org.modeshape.graph.request.DestroyWorkspaceRequest) + */ + @Override + public void process( DestroyWorkspaceRequest request ) { + if (!updatesAllowed(request)) return; + + WorkspaceType workspace = repository.getWorkspace(txn, request.workspaceName()); + if (workspace != null) { + Node root = txn.getRootNode(workspace); + try { + txn.destroyWorkspace(workspace); + request.setActualRootLocation(Location.create(pathFactory.createRootPath(), root.getUuid())); + recordChange(request); + } catch (RuntimeException e) { + request.setError(e); + } + } else { + String msg = GraphI18n.workspaceDoesNotExistInRepository.text(request.workspaceName(), repository.getSourceName()); + request.setError(new InvalidWorkspaceException(msg)); + } + } + + /** + * {@inheritDoc} + * + * @see org.modeshape.graph.request.processor.RequestProcessor#process(org.modeshape.graph.request.GetWorkspacesRequest) + */ + @Override + public void process( GetWorkspacesRequest request ) { + // Always ask the transaction (in case a new workspace was added by another process) ... + Set names = txn.getWorkspaceNames(); + request.setAvailableWorkspaceNames(new HashSet(names)); + setCacheableInfo(request); + } + + /** + * {@inheritDoc} + * + * @see org.modeshape.graph.request.processor.RequestProcessor#process(org.modeshape.graph.request.VerifyWorkspaceRequest) + */ + @Override + public void process( VerifyWorkspaceRequest request ) { + WorkspaceType original = getWorkspace(request, request.workspaceName()); + if (original != null) { + Path path = getExecutionContext().getValueFactories().getPathFactory().createRootPath(); + Node root = txn.getRootNode(original); + request.setActualRootLocation(Location.create(path, root.getUuid())); + request.setActualWorkspaceName(original.getName()); + } + } + + /** + * {@inheritDoc} + * + * @see org.modeshape.graph.request.processor.RequestProcessor#process(org.modeshape.graph.request.CloneWorkspaceRequest) + */ + @Override + public void process( CloneWorkspaceRequest request ) { + if (!updatesAllowed(request)) return; + + // Find the original workspace that we're cloning ... + String targetWorkspaceName = request.desiredNameOfTargetWorkspace(); + String nameOfWorkspaceToBeCloned = request.nameOfWorkspaceToBeCloned(); + WorkspaceType original = repository.getWorkspace(txn, nameOfWorkspaceToBeCloned); + WorkspaceType target = repository.getWorkspace(txn, targetWorkspaceName); + + if (target != null) { + String msg = GraphI18n.workspaceAlreadyExistsInRepository.text(targetWorkspaceName, repository.getSourceName()); + request.setError(new InvalidWorkspaceException(msg)); + return; + } + + if (original == null) { + switch (request.cloneConflictBehavior()) { + case DO_NOT_CLONE: + String msg = GraphI18n.workspaceDoesNotExistInRepository.text(nameOfWorkspaceToBeCloned, + repository.getSourceName()); + request.setError(new InvalidWorkspaceException(msg)); + return; + case SKIP_CLONE: + nameOfWorkspaceToBeCloned = null; + break; + } + } + assert original != null; + target = repository.createWorkspace(txn, targetWorkspaceName, request.targetConflictBehavior(), nameOfWorkspaceToBeCloned); + assert target != null; + NodeType root = txn.getRootNode(target); + request.setActualRootLocation(Location.create(pathFactory.createRootPath(), root.getUuid())); + request.setActualWorkspaceName(target.getName()); + recordChange(request); + } + + /** + * {@inheritDoc} + * + * @see org.modeshape.graph.request.processor.RequestProcessor#process(org.modeshape.graph.request.AccessQueryRequest) + */ + @Override + public void process( AccessQueryRequest request ) { + WorkspaceType workspace = getWorkspace(request, request.workspace()); + if (workspace == null) return; + QueryResults results = txn.query(workspace, request); + if (results != null) { + request.setResults(results.getTuples(), results.getStatistics()); + } else { + super.processUnknownRequest(request); + } + } + + /** + * {@inheritDoc} + * + * @see org.modeshape.graph.request.processor.RequestProcessor#process(org.modeshape.graph.request.FullTextSearchRequest) + */ + @Override + public void process( FullTextSearchRequest request ) { + WorkspaceType workspace = getWorkspace(request, request.workspace()); + if (workspace == null) return; + QueryResults results = txn.search(workspace, request); + if (results != null) { + request.setResults(results.getColumns(), results.getTuples(), results.getStatistics()); + } else { + super.processUnknownRequest(request); + } + } + + protected Location getActualLocation( WorkspaceType workspace, + Location location, + NodeType node ) { + Path path = location.getPath(); + if (path == null) { + // Find the path on the node ... + path = txn.pathFor(workspace, node); + } + // If there is a UUID in the location, it should match the node's. + assert location.getUuid() == null || location.getUuid().equals(node.getUuid()); + if (location.hasIdProperties()) { + return location.with(path); + } + return Location.create(path, node.getUuid()); + } + + protected WorkspaceType getWorkspace( Request request, + String workspaceName ) { + // Get the workspace for this request ... + WorkspaceType workspace = repository.getWorkspace(txn, workspaceName); + if (workspace == null) { + String msg = GraphI18n.workspaceDoesNotExistInRepository.text(workspaceName, repository.getSourceName()); + request.setError(new InvalidWorkspaceException(msg)); + } + return workspace; + } + + protected boolean updatesAllowed( Request request ) { + if (!updatesAllowed) { + request.setError(new InvalidRequestException(GraphI18n.sourceIsReadOnly.text(getSourceName()))); + } + return !request.hasError(); + } + + protected NodeType getTargetNode( WorkspaceType workspace, + Request request, + Location location ) { + if (workspace != null) { + try { + return txn.getNode(workspace, location); + } catch (PathNotFoundException e) { + request.setError(e); + } catch (RuntimeException e) { + request.setError(e); + } + } + return null; + } +} Index: modeshape-graph/src/main/java/org/modeshape/graph/connector/base/Repository.java new file mode 100644 =================================================================== --- /dev/null (revision 1756) +++ modeshape-graph/src/main/java/org/modeshape/graph/connector/base/Repository.java (working copy) @@ -0,0 +1,272 @@ +/* + * 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.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReadWriteLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; +import net.jcip.annotations.ThreadSafe; +import org.modeshape.common.util.CheckArg; +import org.modeshape.graph.ExecutionContext; +import org.modeshape.graph.request.CreateWorkspaceRequest.CreateConflictBehavior; + +/** + * A representation of a repository as a set of named workspaces. Workspaces can be + * {@link #createWorkspace(Transaction, String, CreateConflictBehavior, String) created} or {@link #destroyWorkspace(String) + * destroyed}, though the exact type of {@link Workspace} is dictated by the {@link Transaction}. All workspaces contain a root + * node with the same {@link #getRootNodeUuid() UUID}. + *

+ * Note that this class is thread-safe, since a {@link BaseRepositorySource} will contain a single instance of a concrete subclass + * of this class. Often, the Workspace objects are used to hold onto the workspace-related content, but access to the content is + * always done through a {@link #startTransaction(ExecutionContext, boolean) transaction}. + *

+ * + * @param the node type + * @param the workspace type + */ +@ThreadSafe +public abstract class Repository { + + protected final UUID rootNodeUuid; + protected final ExecutionContext context; + private final String sourceName; + private final String defaultWorkspaceName; + private final Map workspaces = new HashMap(); + private final ReadWriteLock workspacesLock = new ReentrantReadWriteLock(); + + /** + * Creates a {@code MapRepository} with the given repository source name, root node UUID, and a default workspace with the + * given name. + * + * @param context the context in which this repository exists + * @param sourceName the name of the repository source for use in error and informational messages + * @param rootNodeUuid the UUID that will be used as the root node UUID for each workspace in the repository + * @param defaultWorkspaceName the name of the default, auto-created workspace; if null, then a blank name (e.g., "") is + * assumed + * @throws IllegalArgumentException if the repository context or root node UUID are null, or if the source name is null or + * empty + */ + protected Repository( ExecutionContext context, + String sourceName, + UUID rootNodeUuid, + String defaultWorkspaceName ) { + CheckArg.isNotNull(context, "repositoryContext"); + CheckArg.isNotEmpty(sourceName, "sourceName"); + CheckArg.isNotNull(rootNodeUuid, "rootNodeUUID"); + this.context = context; + this.rootNodeUuid = rootNodeUuid; + this.sourceName = sourceName; + this.defaultWorkspaceName = defaultWorkspaceName != null ? defaultWorkspaceName : ""; + } + + /** + * Initializes the repository by creating the default workspace. + *

+ * Due to the ordering restrictions on constructor chaining, this method cannot be called until the repository is fully + * initialized. This method MUST be called at the end of the constructor by any class that implements {@code MapRepository} + * . + */ + protected void initialize() { + Transaction txn = startTransaction(context, false); + try { + // Create the default workspace ... + workspaces.put(this.defaultWorkspaceName, createWorkspace(txn, + this.defaultWorkspaceName, + CreateConflictBehavior.DO_NOT_CREATE, + null)); + } finally { + txn.commit(); + } + + } + + public ExecutionContext getContext() { + return context; + } + + protected String getDefaultWorkspaceName() { + return defaultWorkspaceName; + } + + /** + * Returns the UUID used by the root nodes in each workspace. + *

+ * Note that the root nodes themselves are distinct objects in each workspace and a change to the root node of one workspace + * does not imply a change to the root nodes of any other workspaces. However, the JCR specification mandates that all + * referenceable root nodes in a repository use a common UUID (in support of node correspondence); therefore this must be + * supported by ModeShape. + * + * @return the root node UUID + */ + public final UUID getRootNodeUuid() { + return rootNodeUuid; + } + + /** + * Returns the logical name (as opposed to the class name) of the repository source that defined this instance of the + * repository for use in error, informational, and other contextual messages. + * + * @return sourceName the logical name for the repository source name + */ + public String getSourceName() { + return sourceName; + } + + /** + * Get the names of the available workspaces that have been loaded. + * + * @return the immutable names of the workspaces. + */ + public Set getWorkspaceNames() { + try { + workspacesLock.readLock().lock(); + return Collections.unmodifiableSet(new HashSet(workspaces.keySet())); + } finally { + workspacesLock.readLock().unlock(); + } + } + + /** + * Returns the workspace with the given name. + * + * @param txn the transaction attempting to get the workspace, and which may be used to create the workspace object if needed; + * may not be null + * @param name the name of the workspace to return + * @return the workspace with the given name; may be null if no workspace with the given name exists + */ + public WorkspaceType getWorkspace( Transaction txn, + String name ) { + if (name == null) name = defaultWorkspaceName; + Lock lock = workspacesLock.readLock(); + try { + lock.lock(); + WorkspaceType workspace = workspaces.get(name); + if (workspace == null) { + // Release the read lock and acquire a write lock ... + lock.unlock(); + lock = workspacesLock.writeLock(); + lock.unlock(); + // Try again, in case some other call slipped in ... + workspace = workspaces.get(name); + if (workspace == null) { + workspace = txn.getWorkspace(name, null); + if (workspace != null) { + workspaces.put(workspace.getName(), workspace); + } + } + } + return workspace; + } finally { + lock.unlock(); + } + } + + /** + * Attempts to create a workspace with the given name with name-collision behavior determined by the behavior parameter. + *

+ * This method will first check to see if a workspace already exists with the given name. If no such workspace exists, the + * method will create a new workspace with the given name, add it to the {@code #workspaces workspaces map}, and return it. If + * a workspace with the requested name already exists and the {@code behavior} is {@link CreateConflictBehavior#DO_NOT_CREATE} + * , this method will return {@code null} without modifying the state of the repository. If a workspace with the requested + * name already exists and the {@code behavior} is {@link CreateConflictBehavior#CREATE_WITH_ADJUSTED_NAME}, this method will + * generate a unique new name for the workspace, create a new workspace with the given name, added it to the {@code + * #workspaces workspaces map}, and return it. + *

+ *

+ * If {@code nameOfWorkspaceToClone} is given, this method will clone the content in this original workspace into the new + * workspace. However, if no workspace with the name {@code nameOfWorkspaceToClone} exists, the method will create an empty + * workspace. + *

+ * + * @param txn the transaction creating the workspace; may not be null + * @param name the requested name of the workspace. The name of the workspace that is returned from this method may not be the + * same as the requested name; may not be null + * @param existingWorkspaceBehavior the behavior to use in case a workspace with the requested name already exists in the + * repository + * @param nameOfWorkspaceToClone the name of the workspace from which the content should be cloned; be null if the new + * workspace is to be empty (other than the root node) + * @return the newly created workspace with an exact copy of the contents from the workspace named {@code + * nameOfWorkspaceToClone} or {@code null} if a workspace with the requested name already exists in the repository and + * {@code behavior == CreateConflictBehavior#DO_NOT_CREATE}. + */ + public WorkspaceType createWorkspace( Transaction txn, + String name, + CreateConflictBehavior existingWorkspaceBehavior, + String nameOfWorkspaceToClone ) { + String newName = name; + boolean conflictingName = workspaces.containsKey(newName); + if (conflictingName) { + switch (existingWorkspaceBehavior) { + case DO_NOT_CREATE: + return null; + case CREATE_WITH_ADJUSTED_NAME: + int counter = 0; + do { + newName = name + (++counter); + } while (workspaces.containsKey(newName)); + break; + } + } + assert workspaces.containsKey(newName) == false; + + WorkspaceType original = nameOfWorkspaceToClone != null ? getWorkspace(txn, nameOfWorkspaceToClone) : null; + WorkspaceType workspace = txn.getWorkspace(name, original); + workspaces.put(name, workspace); + return workspace; + } + + /** + * Removes the named workspace from the {@code #workspaces workspaces map}. + * + * @param name the name of the workspace to remove + * @return {@code true} if a workspace with that name previously existed in the map + */ + public boolean destroyWorkspace( String name ) { + try { + workspacesLock.writeLock().lock(); + return workspaces.remove(name) != null; + } finally { + workspacesLock.writeLock().unlock(); + } + } + + /** + * Begin a transaction, hinting whether the transaction will be used only to read the content. If this is called, then the + * transaction must be either {@link Transaction#commit() committed} or {@link Transaction#rollback() rolled back}. + * + * @param context the context in which the transaction is to be performed; may not be null + * @param readonly true if the transaction will not modify any content, or false if changes are to be made + * @return the transaction; never null + * @see Transaction#commit() + * @see Transaction#rollback() + */ + public abstract Transaction startTransaction( ExecutionContext context, + boolean readonly ); +} Index: modeshape-graph/src/main/java/org/modeshape/graph/connector/base/Transaction.java new file mode 100644 =================================================================== --- /dev/null (revision 1756) +++ modeshape-graph/src/main/java/org/modeshape/graph/connector/base/Transaction.java (working copy) @@ -0,0 +1,367 @@ +/* + * 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.Set; +import java.util.UUID; +import net.jcip.annotations.NotThreadSafe; +import org.modeshape.graph.ExecutionContext; +import org.modeshape.graph.Location; +import org.modeshape.graph.connector.LockFailedException; +import org.modeshape.graph.connector.UuidAlreadyExistsException; +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.query.QueryResults; +import org.modeshape.graph.request.AccessQueryRequest; +import org.modeshape.graph.request.FullTextSearchRequest; +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 + * expected to be used by a single thread. + *

+ * + * @param the type of node + * @param the type of workspace + */ +@NotThreadSafe +public interface Transaction { + + /** + * Get the context in which this operator executes. + * + * @return the execution context; never null + */ + ExecutionContext getContext(); + + /** + * Get the names of the existing workspaces. + * + * @return the immutable set of workspace names; never null + */ + Set getWorkspaceNames(); + + /** + * Creates a new workspace with the given name containing only a root node. If the workspace already exists, it is left + * untouched and returned. + * + * @param name the name of the workspace; may not be null + * @param originalToClone the workspace that should be cloned, or null if the new workspace is to only contain a root node + * @return the newly created workspace; may not be null + */ + WorkspaceType getWorkspace( String name, + WorkspaceType originalToClone ); + + /** + * Destroy the workspace with the supplied name. + * + * @param workspace the workspace that is to be destroyed; may not be null + * @return true if the workspace was destroyed, or false if the workspace did not exist + */ + boolean destroyWorkspace( WorkspaceType workspace ); + + /** + * Get the root node of the repository workspace. + * + * @param workspace the workspace; may not be null + * @return the root node; never null + */ + NodeType getRootNode( WorkspaceType workspace ); + + /** + * Find the node with the supplied unique identifier. + * + * @param workspace the workspace; may not be null + * @param location of the node; may not be null + * @return the node, or null if there is no node with the supplied identifier + * @throws PathNotFoundException if the node at the given location could not be found + */ + NodeType getNode( WorkspaceType workspace, + Location location ); + + /** + * Returns the path for the given node with this workspace if one exists, or a {@code null} if no node exists at the given + * path. + * + * @param workspace the workspace; may not be null + * @param node the node for which the path should be retrieved; may not be null + * @return the path for the given node with this workspace if one exists or null if the node does not exist in this workspace + */ + Path pathFor( WorkspaceType workspace, + NodeType node ); + + /** + * Returns the parent of the supplied node. This method returns null if the supplied node is the root node. + * + * @param workspace the workspace; may not be null + * @param node the child node; may not be null + * @return the parent of this node; or null if the node is the root node for its workspace + */ + NodeType getParent( WorkspaceType workspace, + NodeType node ); + + /** + * Find in the supplied parent node the child with the supplied name and same-name-sibling index. This method returns null if + * the parent has no such child. + * + * @param workspace the workspace; may not be null + * @param parent the parent node; may not be null + * @param childSegment the segment of the child; may not be null + * @return the child of this node; or null if no such child exists + */ + NodeType getChild( WorkspaceType workspace, + NodeType parent, + Path.Segment childSegment ); + + /** + * Find in the supplied parent node the first child with the supplied name. This method returns null if the parent has no such + * child. + * + * @param workspace the workspace; may not be null + * @param parent the parent node; may not be null + * @param childName the name of the child; may not be null + * @return the child of this node; or null if no such child exists + */ + NodeType getFirstChild( WorkspaceType workspace, + NodeType parent, + Name childName ); + + /** + * Get the children for the supplied node. + * + * @param workspace the workspace; may not be null + * @param node the node whose children are to be returned; may not be null + * @return the children, never null but possibly empty + */ + List getChildren( WorkspaceType workspace, + NodeType node ); + + /** + * Removes all of the children for this node in a single operation. + * + * @param workspace the workspace; may not be null + * @param node the node whose children are to be removed; may not be null + */ + void removeAllChildren( WorkspaceType workspace, + NodeType node ); + + /** + * Creates a new child node under the supplied parent, where the new child will have the specified name, properties, and + * (optionally) UUID. The child will be appended to the list of children, and will be given the appropriate same-name-sibling + * index. + * + * @param workspace the workspace; may not be null + * @param parent the parent node; may not be null + * @param name the name; may not be null + * @param index index at which the specified child is to be inserted, or -1 if the child is to be appended + * @param uuid the UUID of the node, or null if the UUID is to be generated + * @param properties the properties for the new node; may be null if there are no other properties + * @return the representation of the new node + */ + NodeType addChild( WorkspaceType workspace, + NodeType parent, + Name name, + int index, + UUID uuid, + Iterable properties ); + + /** + * Inserts the specified child at the specified position in the list of children. Shifts the child currently at that position + * (if any) and any subsequent children to the right (adds one to their indices). The child is automatically removed from its + * existing parent (if it has one), though this method can be used to reorder a child within the same parent. + * + * @param workspace the workspace; may not be null + * @param parent the parent node; may not be null + * @param newChild the node that is to be added as a child of the parent; may not be null + * @param beforeOtherChild the existing child before which the child is to be added; may be null if the child is to be added + * at the end + * @param desiredName the desired name for the node; may be null if the new child node's name is to be kept + * @return the actual location of the node, or null if the node didn't exist + */ + Location addChild( WorkspaceType workspace, + NodeType parent, + NodeType newChild, + NodeType beforeOtherChild, + Name desiredName ); + + /** + * Removes the given node from the repository. + * + * @param workspace the workspace; may not be null + * @param node the node to be removed; may not be null + * @return the actual location of the node, or null if the node didn't exist + */ + Location removeNode( WorkspaceType workspace, + NodeType node ); + + /** + * Sets the given properties in a single operation, overwriting any previous properties for the same name. This bulk mutator + * should be used when multiple properties are being set in order to allow underlying implementations to optimize their access + * to their respective persistent storage mechanism. The implementation should not change the identification + * properties. + * + * @param workspace the workspace; may not be null + * @param node the node; may not be null + * @param propertiesToSet the properties to set; may be null or empty if there are no properties being added or set + * @param propertiesToRemove the names of the properties that are to be removed; may be null or empty if no properties are + * being removed + * @param removeAllExisting true if all existing, non-identification properties should be removed, or false otherwise + * @return this map node + */ + NodeType setProperties( WorkspaceType workspace, + NodeType node, + Iterable propertiesToSet, + Iterable propertiesToRemove, + boolean removeAllExisting ); + + /** + * Removes the property with the given name + * + * @param workspace the workspace; may not be null + * @param node the node; may not be null + * @param propertyName the name of the property to remove; may not be null + * @return this map node + */ + NodeType removeProperty( WorkspaceType workspace, + NodeType node, + Name propertyName ); + + /** + * This should clone the subgraph given by the original node and place the cloned copy under the supplied new parent. Note + * that internal references between nodes within the original subgraph must be reflected as internal nodes within the new + * subgraph. + * + * @param originalWorkspace the workspace containing the original node that is being cloned; may not be null + * @param original the node to be cloned; may not be null + * @param newWorkspace the workspace containing the new parent node; may not be null + * @param newParent the parent where the clone is to be placed; may not be null + * @param desiredName the desired name for the node; if null, the name will be calculated from {@code desiredSegment}; Exactly + * one of {@code desiredSegment} and {@code desiredName} must be non-null + * @param desiredSegment the exact segment at which the clone should be rooted; if null, the name will be inferred from + * {@code desiredName}; Exactly one of {@code desiredSegment} and {@code desiredName} must be non-null + * @param removeExisting true if existing nodes in the new workspace with the same UUIDs as nodes in the branch rooted at + * {@code original} should be removed; if false, a UuidAlreadyExistsException will be thrown if a UUID conflict is + * detected + * @param removedExistingNodes the set into which should be placed all of the existing nodes that were removed as a result of + * this clone operation, or null if these nodes need not be collected + * @return the new node, which is the top of the new subgraph + * @throws UuidAlreadyExistsException if {@code removeExisting} is true and and a UUID in the source tree already exists in + * the new workspace + */ + NodeType cloneNode( WorkspaceType originalWorkspace, + NodeType original, + WorkspaceType newWorkspace, + NodeType newParent, + Name desiredName, + Path.Segment desiredSegment, + boolean removeExisting, + Set removedExistingNodes ) throws UuidAlreadyExistsException; + + /** + * This should copy the subgraph given by the original node and place the new copy under the supplied new parent. Note that + * internal references between nodes within the original subgraph must be reflected as internal nodes within the new subgraph. + * + * @param originalWorkspace the workspace containing the original node that is being cloned; may not be null + * @param original the node to be copied; may not be null + * @param newWorkspace the workspace containing the new parent node; may not be null + * @param newParent the parent where the copy is to be placed; may not be null + * @param desiredName the desired name for the node; if null, the name will be obtained from the original node + * @param recursive true if the copy should be recursive + * @return the new node, which is the top of the new subgraph + */ + NodeType copyNode( WorkspaceType originalWorkspace, + NodeType original, + WorkspaceType newWorkspace, + NodeType newParent, + Name desiredName, + boolean recursive ); + + /** + * Perform a query of this workspace. + * + * @param workspace the workspace to be searched; may not be null + * @param accessQuery the access query; may not be null + * @return the query results, or null if the query is not supported + */ + QueryResults query( WorkspaceType workspace, + AccessQueryRequest accessQuery ); + + /** + * Perform a full-text search of this workspace. + * + * @param workspace the workspace to be searched; may not be null + * @param search the full-text search; may not be null + * @return the query results, or null if the query is not supported + */ + QueryResults search( WorkspaceType workspace, + FullTextSearchRequest search ); + + /** + * Attempts to lock the given node with the given timeout. If the lock attempt fails, a {@link LockFailedException} will be + * thrown. + * + * @param workspace the workspace; may not be null + * @param node the node to be locked; may not be null + * @param lockScope the scope of the lock (i.e., whether descendants of {@code node} should be included in the lock + * @param lockTimeoutInMillis the maximum lifetime of the lock in milliseconds; zero (0) indicates that the connector default + * should be used + * @throws LockFailedException if the implementing connector supports locking but the lock could not be acquired. + */ + void lockNode( WorkspaceType workspace, + NodeType node, + LockScope lockScope, + long lockTimeoutInMillis ) throws LockFailedException; + + /** + * Attempts to unlock the given node. + * + * @param workspace the workspace; may not be null + * @param node the node to be unlocked; may not be null + */ + void unlockNode( WorkspaceType workspace, + NodeType node ); + + /** + * Commit any changes that have been made to the repository. This method may throw runtime exceptions if there are failures + * committing the changes, but the transaction is still expected to be closed. + * + * @see #rollback() + */ + void commit(); + + /** + * Rollback any changes that have been made to this repository. This method may throw runtime exceptions if there are failures + * rolling back the changes, but the transaction is still expected to be closed. + * + * @see #commit() + */ + void rollback(); + +} Index: modeshape-graph/src/main/java/org/modeshape/graph/connector/base/Workspace.java new file mode 100644 =================================================================== --- /dev/null (revision 1756) +++ modeshape-graph/src/main/java/org/modeshape/graph/connector/base/Workspace.java (working copy) @@ -0,0 +1,39 @@ +/* + * 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; + + +/** + * The {@code MapWorkspace} stores state and other information about a workspace in a way that is independent of + * {@link Transaction}s. + */ +public interface Workspace { + + /** + * Returns the name of the workspace. There can only be one workspace with a given name per repository. + * + * @return the name of the workspace + */ + String getName(); +} Index: modeshape-graph/src/main/java/org/modeshape/graph/connector/federation/FederatedRepositorySource.java =================================================================== --- modeshape-graph/src/main/java/org/modeshape/graph/connector/federation/FederatedRepositorySource.java (revision 1756) +++ modeshape-graph/src/main/java/org/modeshape/graph/connector/federation/FederatedRepositorySource.java (working copy) @@ -39,15 +39,15 @@ import javax.naming.Reference; import javax.naming.StringRefAddr; import javax.naming.spi.ObjectFactory; import net.jcip.annotations.GuardedBy; -import net.jcip.annotations.NotThreadSafe; +import net.jcip.annotations.ThreadSafe; import org.modeshape.common.i18n.I18n; import org.modeshape.common.util.CheckArg; import org.modeshape.common.util.HashCode; import org.modeshape.common.util.NamedThreadFactory; -import org.modeshape.graph.ModeShapeLexicon; import org.modeshape.graph.ExecutionContext; import org.modeshape.graph.GraphI18n; import org.modeshape.graph.Location; +import org.modeshape.graph.ModeShapeLexicon; import org.modeshape.graph.Node; import org.modeshape.graph.Subgraph; import org.modeshape.graph.SubgraphNode; @@ -69,7 +69,7 @@ import org.modeshape.graph.property.ValueFactory; /** * A {@link RepositorySource} for a federated repository. */ -@NotThreadSafe +@ThreadSafe public class FederatedRepositorySource implements RepositorySource, ObjectFactory { /** Index: modeshape-graph/src/main/java/org/modeshape/graph/connector/inmemory/InMemoryRepositorySource.java =================================================================== --- modeshape-graph/src/main/java/org/modeshape/graph/connector/inmemory/InMemoryRepositorySource.java (revision 1756) +++ modeshape-graph/src/main/java/org/modeshape/graph/connector/inmemory/InMemoryRepositorySource.java (working copy) @@ -44,6 +44,7 @@ import javax.naming.Reference; import javax.naming.StringRefAddr; import javax.naming.spi.ObjectFactory; import net.jcip.annotations.GuardedBy; +import net.jcip.annotations.ThreadSafe; import org.modeshape.common.i18n.I18n; import org.modeshape.common.util.CheckArg; import org.modeshape.common.util.StringUtil; @@ -62,6 +63,7 @@ import org.modeshape.graph.request.CreateWorkspaceRequest.CreateConflictBehavior * A {@link RepositorySource} for an in-memory repository. Each {@link InMemoryRepositorySource} instance contains its own * repository, and the lifetime of the source dictates the lifetime of the repository and its content. */ +@ThreadSafe public class InMemoryRepositorySource implements MapRepositorySource, ObjectFactory { /** @@ -385,7 +387,6 @@ public class InMemoryRepositorySource implements MapRepositorySource, ObjectFact * * @return the names of the workspaces that this source starts with, or null if there are no such workspaces * @see #setPredefinedWorkspaceNames(String[]) - * @see #setCreatingWorkspacesAllowed(boolean) */ public synchronized String[] getPredefinedWorkspaceNames() { String[] copy = new String[predefinedWorkspaces.length]; @@ -398,7 +399,6 @@ public class InMemoryRepositorySource implements MapRepositorySource, ObjectFact * * @param predefinedWorkspaceNames the names of the workspaces that this source should start with, or null if there are no * such workspaces - * @see #setCreatingWorkspacesAllowed(boolean) * @see #getPredefinedWorkspaceNames() */ public synchronized void setPredefinedWorkspaceNames( String[] predefinedWorkspaceNames ) { Index: modeshape-graph/src/main/java/org/modeshape/graph/connector/memory/InMemoryNode.java new file mode 100644 =================================================================== --- /dev/null (revision 1756) +++ modeshape-graph/src/main/java/org/modeshape/graph/connector/memory/InMemoryNode.java (working copy) @@ -0,0 +1,96 @@ +/* + * 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.memory; + +import java.util.List; +import java.util.Map; +import java.util.UUID; +import org.modeshape.graph.connector.base.MapNode; +import org.modeshape.graph.property.Name; +import org.modeshape.graph.property.Property; +import org.modeshape.graph.property.Path.Segment; + +/** + * A specialization of the {@link MapNode}. + */ +public class InMemoryNode extends MapNode { + + private static final long serialVersionUID = 1L; + + /** + * Create a new in-memory node. + * + * @param uuid the desired UUID; never null + * @param name the name of the new node; may be null if the name is not known and there is no parent + * @param parent the UUID of the parent node; may be null if this is the root node and there is no name + * @param properties the properties; may be null if there are no properties + * @param children the list of child nodes; may be null + */ + public InMemoryNode( UUID uuid, + Segment name, + UUID parent, + Map properties, + List children ) { + super(uuid, name, parent, properties, children); + } + + /** + * Create a new in-memory node. + * + * @param uuid the desired UUID; never null + * @param name the name of the new node; may be null if the name is not known and there is no parent + * @param parent the UUID of the parent node; may be null if this is the root node and there is no name + * @param properties the properties; may be null if there are no properties + * @param children the list of child nodes; may be null + */ + public InMemoryNode( UUID uuid, + Segment name, + UUID parent, + Iterable properties, + List children ) { + super(uuid, name, parent, properties, children); + } + + /** + * Create a new in-memory node. + * + * @param uuid the desired UUID; never null + */ + public InMemoryNode( UUID uuid ) { + super(uuid); + } + + /** + * {@inheritDoc} + * + * @see org.modeshape.graph.connector.base.MapNode#freeze() + */ + @Override + public InMemoryNode freeze() { + if (!hasChanges()) return this; + return new InMemoryNode(getUuid(), getName(), getParent(), changes.getUnmodifiableProperties(), + changes.getUnmodifiableChildren()); + } + +} Index: modeshape-graph/src/main/java/org/modeshape/graph/connector/memory/InMemoryRepository.java new file mode 100644 =================================================================== --- /dev/null (revision 1756) +++ modeshape-graph/src/main/java/org/modeshape/graph/connector/memory/InMemoryRepository.java (working copy) @@ -0,0 +1,69 @@ +/* + * 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.memory; + +import java.util.UUID; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReadWriteLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; +import net.jcip.annotations.ThreadSafe; +import org.modeshape.graph.ExecutionContext; +import org.modeshape.graph.connector.base.Repository; + +/** + * The representation of an in-memory repository and its content. + */ +@ThreadSafe +public class InMemoryRepository extends Repository { + + protected final ReadWriteLock lock = new ReentrantReadWriteLock(); + + public InMemoryRepository( ExecutionContext context, + String sourceName, + UUID rootNodeUuid ) { + super(context, sourceName, rootNodeUuid, null); + initialize(); + } + + public InMemoryRepository( ExecutionContext context, + String sourceName, + UUID rootNodeUuid, + String defaultWorkspaceName ) { + super(context, sourceName, rootNodeUuid, defaultWorkspaceName); + initialize(); + } + + /** + * {@inheritDoc} + * + * @see org.modeshape.graph.connector.base.Repository#startTransaction(org.modeshape.graph.ExecutionContext, boolean) + */ + @Override + public InMemoryTransaction startTransaction( ExecutionContext context, + boolean readonly ) { + final Lock lock = readonly ? this.lock.readLock() : this.lock.writeLock(); + lock.lock(); + return new InMemoryTransaction(this, getRootNodeUuid(), lock); + } +} Index: modeshape-graph/src/main/java/org/modeshape/graph/connector/memory/InMemoryRepositorySource.java new file mode 100644 =================================================================== --- /dev/null (revision 1756) +++ modeshape-graph/src/main/java/org/modeshape/graph/connector/memory/InMemoryRepositorySource.java (working copy) @@ -0,0 +1,448 @@ +/* + * 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.memory; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Hashtable; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.atomic.AtomicInteger; +import javax.naming.BinaryRefAddr; +import javax.naming.Context; +import javax.naming.InitialContext; +import javax.naming.NamingException; +import javax.naming.RefAddr; +import javax.naming.Reference; +import javax.naming.StringRefAddr; +import javax.naming.spi.ObjectFactory; +import net.jcip.annotations.GuardedBy; +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.graph.ExecutionContext; +import org.modeshape.graph.GraphI18n; +import org.modeshape.graph.cache.CachePolicy; +import org.modeshape.graph.connector.RepositoryConnection; +import org.modeshape.graph.connector.RepositoryContext; +import org.modeshape.graph.connector.RepositorySource; +import org.modeshape.graph.connector.RepositorySourceCapabilities; +import org.modeshape.graph.connector.RepositorySourceException; +import org.modeshape.graph.connector.base.BaseRepositorySource; +import org.modeshape.graph.connector.base.Connection; +import org.modeshape.graph.request.CreateWorkspaceRequest.CreateConflictBehavior; + +/** + * A {@link RepositorySource} for an in-memory repository. Each {@link InMemoryRepositorySource} instance contains its own + * repository, and the lifetime of the source dictates the lifetime of the repository and its content. + */ +@ThreadSafe +public class InMemoryRepositorySource implements BaseRepositorySource, ObjectFactory { + + /** + * The initial version is 1 + */ + private static final long serialVersionUID = 1L; + + /** + * The default limit is {@value} for retrying {@link RepositoryConnection connection} calls to the underlying source. + */ + public static final int DEFAULT_RETRY_LIMIT = 0; + + /** + * The default name for the workspace used by this source, which is a blank string. + */ + public static final String DEFAULT_WORKSPACE_NAME = ""; + + protected static final RepositorySourceCapabilities CAPABILITIES = new RepositorySourceCapabilities(true, true, false, true, + true); + + protected static final String ROOT_NODE_UUID_ATTR = "rootNodeUuid"; + protected static final String SOURCE_NAME_ATTR = "sourceName"; + protected static final String PREDEFINED_WORKSPACE_NAMES = "predefinedWorkspaceNames"; + protected static final String DEFAULT_WORKSPACE_NAME_ATTR = "defaultWorkspaceName"; + protected static final String DEFAULT_CACHE_POLICY_ATTR = "defaultCachePolicy"; + protected static final String JNDI_NAME_ATTR = "jndiName"; + protected static final String RETRY_LIMIT_ATTR = "retryLimit"; + + @GuardedBy( "sourcesLock" ) + private String name; + @GuardedBy( "this" ) + private String jndiName; + private String defaultWorkspaceName = DEFAULT_WORKSPACE_NAME; + private UUID rootNodeUuid = UUID.randomUUID(); + private CachePolicy defaultCachePolicy; + private volatile String[] predefinedWorkspaces = new String[] {}; + private final AtomicInteger retryLimit = new AtomicInteger(DEFAULT_RETRY_LIMIT); + private transient InMemoryRepository repository; + private transient RepositoryContext repositoryContext; + private transient ExecutionContext defaultContext = new ExecutionContext(); + + /** + * Create a repository source instance. + */ + public InMemoryRepositorySource() { + super(); + } + + /** + * {@inheritDoc} + * + * @see org.modeshape.graph.connector.RepositorySource#initialize(org.modeshape.graph.connector.RepositoryContext) + */ + public void initialize( RepositoryContext context ) throws RepositorySourceException { + this.repositoryContext = context; + } + + /** + * @return repositoryContext + */ + public RepositoryContext getRepositoryContext() { + return repositoryContext; + } + + /** + * {@inheritDoc} + * + * @see org.modeshape.graph.connector.RepositorySource#getRetryLimit() + */ + public int getRetryLimit() { + return retryLimit.get(); + } + + /** + * {@inheritDoc} + * + * @see org.modeshape.graph.connector.RepositorySource#setRetryLimit(int) + */ + public void setRetryLimit( int limit ) { + retryLimit.set(limit < 0 ? 0 : limit); + } + + /** + * Get the default cache policy for this source, or null if the global default cache policy should be used + * + * @return the default cache policy, or null if this source has no explicit default cache policy + */ + public CachePolicy getDefaultCachePolicy() { + return defaultCachePolicy; + } + + /** + * @param defaultCachePolicy Sets defaultCachePolicy to the specified value. + */ + public void setDefaultCachePolicy( CachePolicy defaultCachePolicy ) { + this.defaultCachePolicy = defaultCachePolicy; + } + + /** + * Get the name of the workspace that should be used by default. + * + * @return the name of the default workspace + */ + public String getDefaultWorkspaceName() { + return defaultWorkspaceName; + } + + /** + * Set the default workspace name. + * + * @param defaultWorkspaceName the name of the workspace that should be used by default, or null if "" should be used + */ + public void setDefaultWorkspaceName( String defaultWorkspaceName ) { + this.defaultWorkspaceName = defaultWorkspaceName != null ? defaultWorkspaceName : DEFAULT_WORKSPACE_NAME; + } + + /** + * @return rootNodeUuid + */ + public UUID getRootNodeUuid() { + return this.rootNodeUuid; + } + + /** + * @param rootNodeUuid Sets rootNodeUuid to the specified value. + */ + public void setRootNodeUuid( UUID rootNodeUuid ) { + this.rootNodeUuid = rootNodeUuid != null ? rootNodeUuid : UUID.randomUUID(); + } + + /** + * If you use this to set a JNDI name, this source will be bound to that name using the default {@link InitialContext}. You + * can also do this manually if you have additional requirements. + * + * @param name the JNDI name + * @throws NamingException if there is a problem registering this object + * @see #getJndiName() + */ + public void setJndiName( String name ) throws NamingException { + setJndiName(name, null); + } + + /** + * Register this source in JNDI under the supplied name using the supplied context. to set a JNDI name, this source will be + * bound to that name using the default {@link InitialContext}. You can also do this manually if you have additional + * requirements. + * + * @param name the JNDI name, or null if this object is to no longer be registered + * @param context the JNDI context, or null if the {@link InitialContext} should be used + * @throws NamingException if there is a problem registering this object + * @see #getJndiName() + */ + public synchronized void setJndiName( String name, + Context context ) throws NamingException { + CheckArg.isNotNull(name, "name"); + if (context == null) context = new InitialContext(); + + // First register in JNDI under the new name ... + if (name != null) { + context.bind(name, this); + } + // Second, unregister from JNDI if there is already a name ... + if (jndiName != null && !jndiName.equals(name)) { + context.unbind(jndiName); + } + // Record the new name ... + this.jndiName = name; + } + + /** + * Gets the JNDI name this source is bound to. Only valid if you used setJNDIName to bind it. + * + * @return the JNDI name, or null if it is not bound in JNDI + * @see #setJndiName(String) + */ + public synchronized String getJndiName() { + return jndiName; + } + + /** + * {@inheritDoc} + */ + public String getName() { + return this.name; + } + + /** + * @param name Sets name to the specified value. + */ + public void setName( String name ) { + this.name = name; + } + + /** + * {@inheritDoc} + * + * @see org.modeshape.graph.connector.RepositorySource#getConnection() + */ + public synchronized RepositoryConnection getConnection() throws RepositorySourceException { + if (repository == null) { + ExecutionContext context = repositoryContext != null ? repositoryContext.getExecutionContext() : defaultContext; + repository = new InMemoryRepository(context, name, rootNodeUuid, defaultWorkspaceName); + + // Create the set of initial workspaces ... + for (String initialName : getPredefinedWorkspaceNames()) { + repository.createWorkspace(null, initialName, CreateConflictBehavior.DO_NOT_CREATE, null); + } + } + return new Connection(this, repository); + } + + /** + * {@inheritDoc} + * + * @see org.modeshape.graph.connector.RepositorySource#close() + */ + public synchronized void close() { + // Null the reference to the in-memory repository; open connections still reference it and can continue to work ... + this.repository = null; + } + + /** + * {@inheritDoc} + */ + public synchronized Reference getReference() { + String className = getClass().getName(); + String factoryClassName = this.getClass().getName(); + Reference ref = new Reference(className, factoryClassName, null); + + if (getName() != null) { + ref.add(new StringRefAddr(SOURCE_NAME_ATTR, getName())); + } + if (getRootNodeUuid() != null) { + ref.add(new StringRefAddr(ROOT_NODE_UUID_ATTR, getRootNodeUuid().toString())); + } + if (getJndiName() != null) { + ref.add(new StringRefAddr(JNDI_NAME_ATTR, getJndiName())); + } + if (getDefaultWorkspaceName() != null) { + ref.add(new StringRefAddr(DEFAULT_WORKSPACE_NAME_ATTR, getDefaultWorkspaceName())); + } + String[] workspaceNames = getPredefinedWorkspaceNames(); + if (workspaceNames != null && workspaceNames.length != 0) { + ref.add(new StringRefAddr(PREDEFINED_WORKSPACE_NAMES, StringUtil.combineLines(workspaceNames))); + } + if (getDefaultCachePolicy() != null) { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + CachePolicy policy = getDefaultCachePolicy(); + try { + ObjectOutputStream oos = new ObjectOutputStream(baos); + oos.writeObject(policy); + ref.add(new BinaryRefAddr(DEFAULT_CACHE_POLICY_ATTR, baos.toByteArray())); + } catch (IOException e) { + I18n msg = GraphI18n.errorSerializingInMemoryCachePolicyInSource; + throw new RepositorySourceException(getName(), msg.text(policy.getClass().getName(), getName()), e); + } + } + ref.add(new StringRefAddr(RETRY_LIMIT_ATTR, Integer.toString(getRetryLimit()))); + return ref; + } + + /** + * {@inheritDoc} + */ + public Object getObjectInstance( Object obj, + javax.naming.Name name, + Context nameCtx, + Hashtable environment ) throws Exception { + if (obj instanceof Reference) { + Map values = new HashMap(); + Reference ref = (Reference)obj; + 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); + } + } + } + String sourceName = (String)values.get(SOURCE_NAME_ATTR); + String rootNodeUuidString = (String)values.get(ROOT_NODE_UUID_ATTR); + String jndiName = (String)values.get(JNDI_NAME_ATTR); + String defaultWorkspaceName = (String)values.get(DEFAULT_WORKSPACE_NAME_ATTR); + Object defaultCachePolicy = values.get(DEFAULT_CACHE_POLICY_ATTR); + String retryLimit = (String)values.get(RETRY_LIMIT_ATTR); + + String combinedWorkspaceNames = (String)values.get(PREDEFINED_WORKSPACE_NAMES); + String[] workspaceNames = null; + if (combinedWorkspaceNames != null) { + List paths = StringUtil.splitLines(combinedWorkspaceNames); + workspaceNames = paths.toArray(new String[paths.size()]); + } + + // Create the source instance ... + InMemoryRepositorySource source = new InMemoryRepositorySource(); + if (sourceName != null) source.setName(sourceName); + if (rootNodeUuidString != null) source.setRootNodeUuid(UUID.fromString(rootNodeUuidString)); + if (defaultWorkspaceName != null) source.setDefaultWorkspaceName(defaultWorkspaceName); + if (jndiName != null) source.setJndiName(jndiName); + if (defaultCachePolicy instanceof CachePolicy) { + source.setDefaultCachePolicy((CachePolicy)defaultCachePolicy); + } + if (workspaceNames != null && workspaceNames.length != 0) source.setPredefinedWorkspaceNames(workspaceNames); + if (retryLimit != null) source.setRetryLimit(Integer.parseInt(retryLimit)); + return source; + } + return null; + } + + /** + * Gets the names of the workspaces that are available when this source is created. + * + * @return the names of the workspaces that this source starts with, or null if there are no such workspaces + * @see #setPredefinedWorkspaceNames(String[]) + */ + public synchronized String[] getPredefinedWorkspaceNames() { + String[] copy = new String[predefinedWorkspaces.length]; + System.arraycopy(predefinedWorkspaces, 0, copy, 0, predefinedWorkspaces.length); + return copy; + } + + /** + * Sets the names of the workspaces that are available when this source is created. + * + * @param predefinedWorkspaceNames the names of the workspaces that this source should start with, or null if there are no + * such workspaces + * @see #getPredefinedWorkspaceNames() + */ + public synchronized void setPredefinedWorkspaceNames( String[] predefinedWorkspaceNames ) { + this.predefinedWorkspaces = predefinedWorkspaceNames; + } + + /** + * {@inheritDoc} + * + * @see org.modeshape.graph.connector.RepositorySource#getCapabilities() + */ + public RepositorySourceCapabilities getCapabilities() { + return CAPABILITIES; + } + + public boolean areUpdatesAllowed() { + return true; + } + + /** + * In-memory connectors aren't shared and cannot be loaded from external sources if updates are not allowed. Therefore, in + * order to avoid setting up an in-memory connector that is permanently empty (presumably, not a desired outcome), all + * in-memory connectors must allow updates. + * + * @param updatesAllowed must be true + * @throws RepositorySourceException if {@code updatesAllowed != true}. + */ + public void setUpdatesAllowed( boolean updatesAllowed ) { + if (updatesAllowed == false) { + throw new RepositorySourceException(GraphI18n.inMemoryConnectorMustAllowUpdates.text(this.name)); + } + + } + + /** + * {@inheritDoc} + * + * @see java.lang.Object#toString() + */ + @Override + public String toString() { + return "The \"" + name + "\" in-memory repository"; + } +} Index: modeshape-graph/src/main/java/org/modeshape/graph/connector/memory/InMemoryTransaction.java new file mode 100644 =================================================================== --- /dev/null (revision 1756) +++ modeshape-graph/src/main/java/org/modeshape/graph/connector/memory/InMemoryTransaction.java (working copy) @@ -0,0 +1,128 @@ +/* + * 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.memory; + +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.locks.Lock; +import net.jcip.annotations.NotThreadSafe; +import org.modeshape.graph.connector.base.MapTransaction; +import org.modeshape.graph.property.Property; +import org.modeshape.graph.property.Path.Segment; + +/** + * + */ +@NotThreadSafe +public class InMemoryTransaction extends MapTransaction { + + private final InMemoryRepository repository; + private final Lock lock; + + protected InMemoryTransaction( InMemoryRepository repository, + UUID rootNodeUuid, + Lock lock ) { + super(repository, rootNodeUuid); + this.repository = repository; + this.lock = lock; + assert this.lock != null; + } + + /** + * {@inheritDoc} + * + * @see org.modeshape.graph.connector.base.Transaction#getWorkspaceNames() + */ + public Set getWorkspaceNames() { + return repository.getWorkspaceNames(); + } + + /** + * {@inheritDoc} + * + * @see org.modeshape.graph.connector.base.Transaction#getWorkspace(java.lang.String, + * org.modeshape.graph.connector.base.Workspace) + */ + public InMemoryWorkspace getWorkspace( String name, + InMemoryWorkspace originalToClone ) { + if (originalToClone != null) { + return new InMemoryWorkspace(name, originalToClone); + } + return new InMemoryWorkspace(name, new InMemoryNode(repository.getRootNodeUuid())); + } + + /** + * {@inheritDoc} + * + * @see org.modeshape.graph.connector.base.Transaction#destroyWorkspace(org.modeshape.graph.connector.base.Workspace) + */ + public boolean destroyWorkspace( InMemoryWorkspace workspace ) { + // The InMemoryRepository is holding onto the Workspace objects for us and will be cleaned up properly, + // so we can just return true here + return true; + } + + /** + * {@inheritDoc} + * + * @see org.modeshape.graph.connector.base.MapTransaction#createNode(java.util.UUID, + * org.modeshape.graph.property.Path.Segment, java.util.UUID, java.lang.Iterable) + */ + @Override + protected InMemoryNode createNode( UUID uuid, + Segment name, + UUID parentUuid, + Iterable properties ) { + return new InMemoryNode(uuid, name, parentUuid, properties, null); + } + + /** + * {@inheritDoc} + * + * @see org.modeshape.graph.connector.base.Transaction#commit() + */ + @Override + public void commit() { + try { + super.commit(); + } finally { + this.lock.unlock(); + } + } + + /** + * {@inheritDoc} + * + * @see org.modeshape.graph.connector.base.Transaction#rollback() + */ + @Override + public void rollback() { + try { + super.rollback(); + } finally { + this.lock.unlock(); + } + } + +} Index: modeshape-graph/src/main/java/org/modeshape/graph/connector/memory/InMemoryWorkspace.java new file mode 100644 =================================================================== --- /dev/null (revision 1756) +++ modeshape-graph/src/main/java/org/modeshape/graph/connector/memory/InMemoryWorkspace.java (working copy) @@ -0,0 +1,55 @@ +/* + * 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.memory; + +import java.util.HashMap; +import java.util.UUID; +import org.modeshape.graph.connector.base.MapWorkspace; + +/** + * + */ +public class InMemoryWorkspace extends MapWorkspace { + + /** + * @param name + * @param rootNode + */ + public InMemoryWorkspace( String name, + InMemoryNode rootNode ) { + super(name, new HashMap(), rootNode); + + } + + /** + * @param name + * @param originalToClone + */ + public InMemoryWorkspace( String name, + InMemoryWorkspace originalToClone ) { + super(name, new HashMap(), originalToClone); + + } + +} Index: modeshape-graph/src/main/java/org/modeshape/graph/connector/memory/package-info.java new file mode 100644 =================================================================== --- /dev/null (revision 1756) +++ modeshape-graph/src/main/java/org/modeshape/graph/connector/memory/package-info.java (working copy) @@ -0,0 +1,31 @@ +/* + * 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. + */ +/** + * The In-Memory Connector represents a {@link org.modeshape.graph.connector.RepositorySource connector} that maintains + * a graph in transient in-process memory. This connector is a good solution for a readable and writable repository source + * that is very lightweight and of small to moderate sizes, but that does require automated persistence. + */ + +package org.modeshape.graph.connector.memory; + Index: modeshape-graph/src/main/java/org/modeshape/graph/connector/path/AbstractPathRepositorySource.java =================================================================== --- modeshape-graph/src/main/java/org/modeshape/graph/connector/path/AbstractPathRepositorySource.java (revision 1756) +++ modeshape-graph/src/main/java/org/modeshape/graph/connector/path/AbstractPathRepositorySource.java (working copy) @@ -34,6 +34,7 @@ import javax.naming.BinaryRefAddr; import javax.naming.RefAddr; import javax.naming.Reference; import javax.naming.StringRefAddr; +import net.jcip.annotations.ThreadSafe; import org.modeshape.common.util.CheckArg; import org.modeshape.graph.connector.RepositoryContext; import org.modeshape.graph.connector.RepositorySourceException; @@ -45,6 +46,7 @@ import org.modeshape.graph.connector.path.cache.PathRepositoryCache; * Basic implementation of the trivial {@link PathRepositorySource} methods and the {@link org.modeshape.graph.connector.path path * repository cache life cycle}. */ +@ThreadSafe public abstract class AbstractPathRepositorySource implements PathRepositorySource { private static final long serialVersionUID = 1L; Index: modeshape-graph/src/main/java/org/modeshape/graph/property/Property.java =================================================================== --- modeshape-graph/src/main/java/org/modeshape/graph/property/Property.java (revision 1756) +++ modeshape-graph/src/main/java/org/modeshape/graph/property/Property.java (working copy) @@ -23,6 +23,7 @@ */ package org.modeshape.graph.property; +import java.io.Serializable; import java.util.Iterator; import net.jcip.annotations.Immutable; @@ -56,7 +57,7 @@ import net.jcip.annotations.Immutable; *

*/ @Immutable -public interface Property extends Iterable, Comparable, Readable { +public interface Property extends Iterable, Comparable, Readable, Serializable { /** * Get the name of the property. Index: modeshape-graph/src/main/java/org/modeshape/graph/request/processor/RequestProcessor.java =================================================================== --- modeshape-graph/src/main/java/org/modeshape/graph/request/processor/RequestProcessor.java (revision 1756) +++ modeshape-graph/src/main/java/org/modeshape/graph/request/processor/RequestProcessor.java (working copy) @@ -313,7 +313,11 @@ public abstract class RequestProcessor { Request embedded = iter.next(); assert embedded != null; if (embedded.isCancelled()) return; - process(embedded); + try { + process(embedded); + } catch (RuntimeException e) { + embedded.setError(e); + } if (!hasErrors && embedded.hasError()) { hasErrors = true; if (!readonly && !embedded.isReadOnly()) { Index: modeshape-graph/src/main/java/org/modeshape/graph/search/SearchableRepositorySource.java =================================================================== --- modeshape-graph/src/main/java/org/modeshape/graph/search/SearchableRepositorySource.java (revision 1756) +++ modeshape-graph/src/main/java/org/modeshape/graph/search/SearchableRepositorySource.java (working copy) @@ -33,6 +33,7 @@ import javax.naming.NamingException; import javax.naming.Reference; import javax.transaction.xa.XAResource; import net.jcip.annotations.NotThreadSafe; +import net.jcip.annotations.ThreadSafe; import org.modeshape.common.i18n.I18n; import org.modeshape.common.util.CheckArg; import org.modeshape.common.util.Logger; @@ -61,6 +62,7 @@ import org.modeshape.graph.request.processor.RequestProcessor; * {@link RepositorySourceCapabilities#supportsQueries() non-querable} RepositorySource instance to provide search and query * capability. */ +@ThreadSafe public class SearchableRepositorySource implements RepositorySource { private static final long serialVersionUID = 1L; Index: modeshape-graph/src/test/java/org/modeshape/graph/connector/TimeDelayingRepositorySource.java =================================================================== --- modeshape-graph/src/test/java/org/modeshape/graph/connector/TimeDelayingRepositorySource.java (revision 1756) +++ modeshape-graph/src/test/java/org/modeshape/graph/connector/TimeDelayingRepositorySource.java (working copy) @@ -40,8 +40,6 @@ import org.modeshape.graph.request.Request; /** * A simple {@link RepositorySource} that simulates an imaginary source with a built-in delay mechanism. - * - * @author Randall Hauch */ @ThreadSafe public class TimeDelayingRepositorySource implements RepositorySource { Index: modeshape-graph/src/test/java/org/modeshape/graph/connector/base/MapNodeTest.java new file mode 100644 =================================================================== --- /dev/null (revision 1756) +++ modeshape-graph/src/test/java/org/modeshape/graph/connector/base/MapNodeTest.java (working copy) @@ -0,0 +1,163 @@ +/* + * 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 static org.hamcrest.core.Is.is; +import static org.hamcrest.core.IsNot.not; +import static org.hamcrest.core.IsNull.nullValue; +import static org.hamcrest.core.IsSame.sameInstance; +import static org.junit.Assert.assertThat; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import org.junit.Before; +import org.junit.Test; +import org.modeshape.graph.ExecutionContext; +import org.modeshape.graph.property.Name; +import org.modeshape.graph.property.Property; +import org.modeshape.graph.property.Path.Segment; + +public class MapNodeTest { + + private MapNode node; + private ExecutionContext context; + + @Before + public void beforeEach() { + context = new ExecutionContext(); + + UUID uuid = UUID.randomUUID(); + Segment name = segment("myNode[3]"); + UUID parent = UUID.randomUUID(); + List props = properties(property("propA", "valueA"), property("propB", "valueB")); + node = new MapNode(uuid, name, parent, props, null); + } + + public Name name( String name ) { + return context.getValueFactories().getNameFactory().create(name); + } + + public Segment segment( String segment ) { + return context.getValueFactories().getPathFactory().createSegment(segment); + } + + public Property property( String name, + Object... values ) { + return context.getPropertyFactory().create(name(name), values); + } + + public List properties( Property... properties ) { + return Arrays.asList(properties); + } + + @Test + public void shouldInstantiateNodeWithUuid() { + UUID uuid = UUID.randomUUID(); + node = new MapNode(uuid); + assertThat(node.getUuid(), is(sameInstance(uuid))); + assertThat(node.getParent(), is(nullValue())); + assertThat(node.getName(), is(nullValue())); + assertThat(node.getChildren().isEmpty(), is(true)); + assertThat(node.getProperties().isEmpty(), is(true)); + } + + @Test + public void shouldInstantiateNodeWithUuidAndNameAndParent() { + UUID uuid = UUID.randomUUID(); + Segment name = segment("myNode[3]"); + UUID parent = UUID.randomUUID(); + node = new MapNode(uuid, name, parent, (Map)null, null); + assertThat(node.getUuid(), is(sameInstance(uuid))); + assertThat(node.getParent(), is(parent)); + assertThat(node.getName(), is(name)); + assertThat(node.getChildren().isEmpty(), is(true)); + assertThat(node.getProperties().isEmpty(), is(true)); + } + + @Test + public void shouldInstantiateNodeWithUuidAndNoNameOrParent() { + UUID uuid = UUID.randomUUID(); + node = new MapNode(uuid, null, null, (Map)null, null); + assertThat(node.getUuid(), is(sameInstance(uuid))); + assertThat(node.getParent(), is(nullValue())); + assertThat(node.getName(), is(nullValue())); + assertThat(node.getChildren().isEmpty(), is(true)); + assertThat(node.getProperties().isEmpty(), is(true)); + } + + @Test + public void shouldInstantiateNodeWithUuidAndNameAndParentAndProperties() { + UUID uuid = UUID.randomUUID(); + Segment name = segment("myNode[3]"); + UUID parent = UUID.randomUUID(); + List props = properties(property("propA", "valueA"), property("propB", "valueB")); + node = new MapNode(uuid, name, parent, props, null); + assertThat(node.getUuid(), is(sameInstance(uuid))); + assertThat(node.getParent(), is(parent)); + assertThat(node.getName(), is(name)); + assertThat(node.getChildren().isEmpty(), is(true)); + assertThat(node.getProperties().size(), is(2)); + assertThat(node.getProperty(name("propA")), is(props.get(0))); + assertThat(node.getProperty(name("propB")), is(props.get(1))); + } + + @Test + public void shouldCreateCopyWithNewParent() { + UUID uuid = UUID.randomUUID(); + assertThat(node.getParent(), is(not(uuid))); + node = node.withParent(uuid); + assertThat(node.getParent(), is(uuid)); + } + + @Test + public void shouldCreateCopyWithNewChild() { + UUID uuid = UUID.randomUUID(); + node = node.withChild(uuid); + assertThat(node.getChildren().size(), is(1)); + assertThat(node.getChildren().get(0), is(uuid)); + } + + @Test + public void shouldCreateCopyWithNewChildAtParticularIndex() { + UUID uuid = UUID.randomUUID(); + node = node.withChild(0, uuid); + assertThat(node.getChildren().size(), is(1)); + assertThat(node.getChildren().get(0), is(uuid)); + + UUID uuid2 = UUID.randomUUID(); + node = node.withChild(1, uuid2); + assertThat(node.getChildren().size(), is(2)); + assertThat(node.getChildren().get(0), is(uuid)); + assertThat(node.getChildren().get(1), is(uuid2)); + + UUID uuid3 = UUID.randomUUID(); + node = node.withChild(1, uuid3); + assertThat(node.getChildren().size(), is(3)); + assertThat(node.getChildren().get(0), is(uuid)); + assertThat(node.getChildren().get(1), is(uuid3)); + assertThat(node.getChildren().get(2), is(uuid2)); + } + +} Index: modeshape-graph/src/test/java/org/modeshape/graph/connector/memory/InMemoryConnectorReadableTest.java new file mode 100644 =================================================================== --- /dev/null (revision 1756) +++ modeshape-graph/src/test/java/org/modeshape/graph/connector/memory/InMemoryConnectorReadableTest.java (working copy) @@ -0,0 +1,82 @@ +/* + * 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.memory; + +import org.junit.Test; +import org.modeshape.common.statistic.Stopwatch; +import org.modeshape.graph.Graph; +import org.modeshape.graph.Location; +import org.modeshape.graph.Subgraph; +import org.modeshape.graph.connector.RepositorySource; +import org.modeshape.graph.connector.test.ReadableConnectorTest; + +public class InMemoryConnectorReadableTest extends ReadableConnectorTest { + + /** + * {@inheritDoc} + * + * @see org.modeshape.graph.connector.test.AbstractConnectorTest#setUpSource() + */ + @Override + protected RepositorySource setUpSource() { + InMemoryRepositorySource source = new InMemoryRepositorySource(); + source.setName("Test Repository"); + return source; + } + + /** + * {@inheritDoc} + * + * @see org.modeshape.graph.connector.test.AbstractConnectorTest#initializeContent(org.modeshape.graph.Graph) + */ + @Override + protected void initializeContent( Graph graph ) { + String initialPath = ""; + int depth = 3; + int numChildrenPerNode = 4; + int numPropertiesPerNode = 7; + Stopwatch sw = new Stopwatch(); + boolean batch = true; + createSubgraph(graph, initialPath, depth, numChildrenPerNode, numPropertiesPerNode, batch, sw, System.out, null); + } + + @Test + public void shouldReturnSameStructureForRepeatedReadBranchRequestsOnLargeRepository() { + // Initialize workspace one with some content ... + String initialPath = ""; + int depth = 4; + int numChildrenPerNode = 10; + int numPropertiesPerNode = 7; + Stopwatch sw = new Stopwatch(); + boolean batch = true; + createSubgraph(graph, initialPath, depth, numChildrenPerNode, numPropertiesPerNode, batch, sw, System.out, null); + + // Verify that the content doesn't change + Location root = graph.getCurrentWorkspace().getRoot(); + Subgraph subgraph1 = graph.getSubgraphOfDepth(10).at(root); + Subgraph subgraph2 = graph.getSubgraphOfDepth(10).at(root); + assertEquivalentSubgraphs(subgraph1, subgraph2, true, true); + } + +} Index: modeshape-graph/src/test/java/org/modeshape/graph/connector/memory/InMemoryConnectorWritableTest.java new file mode 100644 =================================================================== --- /dev/null (revision 1756) +++ modeshape-graph/src/test/java/org/modeshape/graph/connector/memory/InMemoryConnectorWritableTest.java (working copy) @@ -0,0 +1,53 @@ +/* + * 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.memory; + +import org.modeshape.graph.Graph; +import org.modeshape.graph.connector.RepositorySource; +import org.modeshape.graph.connector.test.WritableConnectorTest; + +public class InMemoryConnectorWritableTest extends WritableConnectorTest { + + /** + * {@inheritDoc} + * + * @see org.modeshape.graph.connector.test.AbstractConnectorTest#setUpSource() + */ + @Override + protected RepositorySource setUpSource() { + InMemoryRepositorySource source = new InMemoryRepositorySource(); + source.setName("Test Repository"); + return source; + } + + /** + * {@inheritDoc} + * + * @see org.modeshape.graph.connector.test.AbstractConnectorTest#initializeContent(org.modeshape.graph.Graph) + */ + @Override + protected void initializeContent( Graph graph ) { + } + +} Index: modeshape-graph/src/test/java/org/modeshape/graph/connector/memory/InMemoryRepositoryTest.java new file mode 100644 =================================================================== --- /dev/null (revision 1756) +++ modeshape-graph/src/test/java/org/modeshape/graph/connector/memory/InMemoryRepositoryTest.java (working copy) @@ -0,0 +1,52 @@ +/* + * 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.memory; + +import java.util.UUID; +import org.junit.Before; +import org.junit.Test; +import org.modeshape.graph.ExecutionContext; + +public class InMemoryRepositoryTest { + + private UUID rootUuid; + + private ExecutionContext context; + + @Before + public void beforeEach() throws Exception { + context = new ExecutionContext(); + rootUuid = UUID.randomUUID(); + } + + @Test( expected = IllegalArgumentException.class ) + public void shouldNotAllowNullNameInConstructor() { + new InMemoryRepository(context, null, rootUuid); + } + + @Test( expected = IllegalArgumentException.class ) + public void shouldNotAllowBlankNameInConstructor() { + new InMemoryRepository(context, " \t ", rootUuid); + } +} Index: modeshape-graph/src/test/java/org/modeshape/graph/connector/test/WritableConnectorTest.java =================================================================== --- modeshape-graph/src/test/java/org/modeshape/graph/connector/test/WritableConnectorTest.java (revision 1756) +++ modeshape-graph/src/test/java/org/modeshape/graph/connector/test/WritableConnectorTest.java (working copy) @@ -28,19 +28,22 @@ import static org.hamcrest.core.IsInstanceOf.instanceOf; import static org.hamcrest.core.IsNot.not; import static org.hamcrest.core.IsNull.notNullValue; import static org.hamcrest.core.IsNull.nullValue; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.fail; +import static org.junit.Assume.assumeNoException; import static org.modeshape.graph.IsNodeWithChildren.hasChild; import static org.modeshape.graph.IsNodeWithChildren.hasChildren; import static org.modeshape.graph.IsNodeWithChildren.isEmpty; import static org.modeshape.graph.IsNodeWithProperty.hasProperty; -import static org.junit.Assert.assertThat; -import static org.junit.Assert.fail; -import static org.junit.Assume.assumeNoException; import java.util.UUID; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; import org.modeshape.common.statistic.Stopwatch; import org.modeshape.common.util.IoUtil; -import org.modeshape.graph.ModeShapeLexicon; import org.modeshape.graph.Graph; import org.modeshape.graph.JcrLexicon; +import org.modeshape.graph.ModeShapeLexicon; import org.modeshape.graph.Node; import org.modeshape.graph.Subgraph; import org.modeshape.graph.connector.RepositorySource; @@ -48,9 +51,6 @@ import org.modeshape.graph.connector.UuidAlreadyExistsException; import org.modeshape.graph.property.PathNotFoundException; import org.modeshape.graph.property.PropertyFactory; import org.modeshape.graph.property.Reference; -import org.junit.After; -import org.junit.Before; -import org.junit.Test; /** * A class that provides standard writing verification tests for connectors that are able to store any content (in any structure). @@ -929,7 +929,6 @@ public abstract class WritableConnectorTest extends AbstractConnectorTest { graph.create("/newUuids").and(); // Copy once to get the UUID into the default workspace - // graph.copy("/node1").replacingExistingNodesWithSameUuids().fromWorkspace(workspaceName).to("/newUuids/node1"); graph.clone("/node1") .fromWorkspace(workspaceName) .as(name("node1")) @@ -945,8 +944,7 @@ public abstract class WritableConnectorTest extends AbstractConnectorTest { graph.create("/newUuids/node1/shouldBeRemoved"); // Copy again to test the behavior now that the UUIDs are already in the default workspace - // This should remove /newUuids/node1/shouldBeRemoved - // graph.copy("/node1").replacingExistingNodesWithSameUuids().fromWorkspace(workspaceName).to("/newUuids/otherNode"); + // This should first remove /newUuids/node1, which of course will remove /newUuids/node1/shouldBeRemoved graph.clone("/node1") .fromWorkspace(workspaceName) .as(name("otherNode")) @@ -1745,8 +1743,8 @@ public abstract class WritableConnectorTest extends AbstractConnectorTest { assertThat(graph.getChildren().of("/node3"), hasChildren(segment("node1"), segment("node2[1]"), - segment("node3"), - segment("node2[2]"))); + segment("node2[2]"), + segment("node3"))); assertThat(graph.getChildren().of("/node3/node2[1]"), hasChildren(segment("node1"), segment("node2"), segment("node3"))); assertThat(graph.getChildren().of("/node3/node3"), hasChildren(segment("node1"), segment("node2"), segment("node3"))); assertThat(graph.getChildren().of("/node3/node3/node1"), hasChildren());