Index: extensions/modeshape-connector-svn/src/main/java/org/modeshape/connector/svn/SvnActionExecutor.java deleted file mode 100644 =================================================================== --- extensions/modeshape-connector-svn/src/main/java/org/modeshape/connector/svn/SvnActionExecutor.java (revision 1825) +++ /dev/null (working copy) @@ -1,73 +0,0 @@ -/* - * 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.connector.svn; - -import org.modeshape.connector.scm.ScmAction; -import org.modeshape.connector.scm.ScmActionExecutor; -import org.tmatesoft.svn.core.SVNErrorCode; -import org.tmatesoft.svn.core.SVNErrorMessage; -import org.tmatesoft.svn.core.SVNException; -import org.tmatesoft.svn.core.io.ISVNEditor; -import org.tmatesoft.svn.core.io.SVNRepository; - -/** - */ -public class SvnActionExecutor implements ScmActionExecutor { - - private final SVNRepository repository; - - /** - * @param repository - */ - public SvnActionExecutor( SVNRepository repository ) { - this.repository = repository; - } - - /** - * @return repository - */ - public SVNRepository getRepository() { - return repository; - } - - /** - * @param action - * @param message - * @throws SVNException - */ - public void execute( ScmAction action, - String message ) throws SVNException { - ISVNEditor editor = this.repository.getCommitEditor(message, null); - editor.openRoot(-1); - try { - action.applyAction(editor); - } catch (Exception e) { - SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.UNKNOWN, "This error is appeared: '{0}'", e.getMessage()); - throw new SVNException(err, e); - } - editor.closeDir(); - editor.closeEdit(); - - } -} Index: extensions/modeshape-connector-svn/src/main/java/org/modeshape/connector/svn/SvnRepository.java =================================================================== --- extensions/modeshape-connector-svn/src/main/java/org/modeshape/connector/svn/SvnRepository.java (revision 1825) +++ extensions/modeshape-connector-svn/src/main/java/org/modeshape/connector/svn/SvnRepository.java (working copy) @@ -1,66 +1,26 @@ package org.modeshape.connector.svn; -import java.io.ByteArrayOutputStream; -import java.io.OutputStream; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -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.common.i18n.I18n; -import org.modeshape.connector.scm.ScmAction; -import org.modeshape.connector.svn.mgnt.AddDirectory; -import org.modeshape.connector.svn.mgnt.AddFile; -import org.modeshape.connector.svn.mgnt.DeleteEntry; -import org.modeshape.connector.svn.mgnt.UpdateFile; import org.modeshape.graph.ExecutionContext; -import org.modeshape.graph.JcrLexicon; -import org.modeshape.graph.JcrNtLexicon; -import org.modeshape.graph.ModeShapeIntLexicon; -import org.modeshape.graph.ModeShapeLexicon; -import org.modeshape.graph.NodeConflictBehavior; -import org.modeshape.graph.connector.RepositorySourceException; -import org.modeshape.graph.connector.path.AbstractWritablePathWorkspace; -import org.modeshape.graph.connector.path.DefaultPathNode; -import org.modeshape.graph.connector.path.PathNode; -import org.modeshape.graph.connector.path.WritablePathRepository; -import org.modeshape.graph.connector.path.WritablePathWorkspace; -import org.modeshape.graph.connector.path.cache.WorkspaceCache; -import org.modeshape.graph.property.Binary; -import org.modeshape.graph.property.BinaryFactory; -import org.modeshape.graph.property.DateTimeFactory; -import org.modeshape.graph.property.Name; -import org.modeshape.graph.property.NameFactory; -import org.modeshape.graph.property.NamespaceRegistry; +import org.modeshape.graph.connector.base.PathNode; +import org.modeshape.graph.connector.base.PathTransaction; +import org.modeshape.graph.connector.base.Processor; +import org.modeshape.graph.connector.base.Repository; +import org.modeshape.graph.connector.base.Transaction; +import org.modeshape.graph.observe.Observer; import org.modeshape.graph.property.Path; -import org.modeshape.graph.property.PathFactory; import org.modeshape.graph.property.Property; -import org.modeshape.graph.property.PropertyFactory; import org.modeshape.graph.property.Path.Segment; import org.modeshape.graph.request.InvalidRequestException; -import org.tmatesoft.svn.core.SVNDirEntry; -import org.tmatesoft.svn.core.SVNErrorCode; -import org.tmatesoft.svn.core.SVNErrorMessage; -import org.tmatesoft.svn.core.SVNException; -import org.tmatesoft.svn.core.SVNNodeKind; -import org.tmatesoft.svn.core.SVNProperties; -import org.tmatesoft.svn.core.SVNProperty; -import org.tmatesoft.svn.core.auth.ISVNAuthenticationManager; +import org.modeshape.graph.request.InvalidWorkspaceException; +import org.modeshape.graph.request.MoveBranchRequest; import org.tmatesoft.svn.core.internal.io.dav.DAVRepositoryFactory; import org.tmatesoft.svn.core.internal.io.fs.FSRepositoryFactory; import org.tmatesoft.svn.core.internal.io.svn.SVNRepositoryFactoryImpl; import org.tmatesoft.svn.core.io.SVNRepository; -import org.tmatesoft.svn.core.wc.SVNWCUtil; -public class SvnRepository extends WritablePathRepository { - - private static final String DEFAULT_MIME_TYPE = "application/octet-stream"; - protected static final byte[] EMPTY_BYTE_ARRAY = new byte[0]; +public class SvnRepository extends Repository { protected final SvnRepositorySource source; @@ -80,809 +40,94 @@ public class SvnRepository extends WritablePathRepository { initialize(); } - @Override - protected void initialize() { - ExecutionContext context = source.getRepositoryContext().getExecutionContext(); - for (String workspaceName : source.getPredefinedWorkspaceNames()) { - doCreateWorkspace(context, workspaceName); - } - - String defaultWorkspaceName = source.getDefaultWorkspaceName(); - if (defaultWorkspaceName != null && !workspaces.containsKey(defaultWorkspaceName)) { - doCreateWorkspace(context, defaultWorkspaceName); - } - + final SvnRepositorySource source() { + return this.source; } - public WorkspaceCache getCache( String workspaceName ) { - return source.getPathRepositoryCache().getCache(workspaceName); + @Override + public SvnTransaction startTransaction( ExecutionContext context, + boolean readonly ) { + return new SvnTransaction(); } /** - * Internal method that creates a workspace and adds it to the map of active workspaces without checking to see if the source - * allows creating workspaces. This is useful when setting up predefined workspaces. - * - * @param context the current execution context; may not be null - * @param name the name of the workspace to create; may not be null - * @return the newly created workspace; never null + * Implementation of the {@link PathTransaction} interface for the Subversion connector */ - private WritablePathWorkspace doCreateWorkspace( ExecutionContext context, - String name ) { - SvnWorkspace workspace = new SvnWorkspace(name, source.getRootNodeUuid()); - - workspaces.putIfAbsent(name, workspace); - return (WritablePathWorkspace)workspaces.get(name); - - } - - @Override - protected WritablePathWorkspace createWorkspace( ExecutionContext context, - String name ) { - if (!source.isCreatingWorkspacesAllowed()) { - String msg = SvnRepositoryConnectorI18n.unableToCreateWorkspaces.text(getSourceName(), name); - throw new InvalidRequestException(msg); - } - - return doCreateWorkspace(context, name); - } - - class SvnWorkspace extends AbstractWritablePathWorkspace { - - /** - * Only certain properties are tolerated when writing content (dna:resource or jcr:resource) nodes. These properties are - * implicitly stored (primary type, data) or silently ignored (encoded, mimetype, last modified). The silently ignored - * properties must be accepted to stay compatible with the JCR specification. - */ - private final Set ALLOWABLE_PROPERTIES_FOR_CONTENT = Collections.unmodifiableSet(new HashSet( - Arrays.asList(new Name[] { - JcrLexicon.PRIMARY_TYPE, - JcrLexicon.DATA, - JcrLexicon.ENCODED, - JcrLexicon.MIMETYPE, - JcrLexicon.LAST_MODIFIED, - JcrLexicon.UUID, - ModeShapeIntLexicon.NODE_DEFINITON}))); - /** - * Only certain properties are tolerated when writing files (nt:file) or folders (nt:folder) nodes. These properties are - * implicitly stored in the file or folder (primary type, created). - */ - private final Set ALLOWABLE_PROPERTIES_FOR_FILE_OR_FOLDER = Collections.unmodifiableSet(new HashSet( - Arrays.asList(new Name[] { - JcrLexicon.PRIMARY_TYPE, - JcrLexicon.CREATED, - JcrLexicon.UUID, - ModeShapeIntLexicon.NODE_DEFINITON}))); - - private final SVNRepository workspaceRoot; - - public SvnWorkspace( String name, - UUID rootNodeUuid ) { - super(name, rootNodeUuid); - - workspaceRoot = getWorkspaceDirectory(name); - - ISVNAuthenticationManager authManager = SVNWCUtil.createDefaultAuthenticationManager(source.getUsername(), - source.getPassword()); - workspaceRoot.setAuthenticationManager(authManager); - } - - public Path getLowestExistingPath( Path path ) { - do { - path = path.getParent(); - - if (getNode(path) != null) { - return path; - } - } while (path != null); - - assert false : "workspace root path was not a valid path"; - return null; - } - - public PathNode getNode( Path path ) { - WorkspaceCache cache = getCache(getName()); - - PathNode node = cache.get(path); - if (node != null) return node; - - ExecutionContext context = source.getRepositoryContext().getExecutionContext(); - List properties = new LinkedList(); - List children = new LinkedList(); - - try { - boolean result = readNode(context, this.getName(), path, properties, children); - if (!result) return null; - } catch (SVNException ex) { - return null; - } - - UUID uuid = path.isRoot() ? source.getRootNodeUuid() : null; - node = new DefaultPathNode(path, uuid, properties, children); - - cache.set(node); - return node; - } - - public PathNode createNode( ExecutionContext context, - PathNode parentNode, - Name name, - Map properties, - NodeConflictBehavior conflictBehavior ) { - - NamespaceRegistry registry = context.getNamespaceRegistry(); - NameFactory nameFactory = context.getValueFactories().getNameFactory(); - PathFactory pathFactory = context.getValueFactories().getPathFactory(); - - // New name to commit into the svn repos workspace - String newName = name.getString(registry); - - Property primaryTypeProp = properties.get(JcrLexicon.PRIMARY_TYPE); - Name primaryType = primaryTypeProp == null ? null : nameFactory.create(primaryTypeProp.getFirstValue()); - - Path parentPath = parentNode.getPath(); - String parentPathAsString = parentPath.getString(registry); - Path newPath = pathFactory.create(parentPath, name); - - String newChildPath = null; - - // File - if (JcrNtLexicon.FILE.equals(primaryType)) { - ensureValidProperties(context, properties.values(), ALLOWABLE_PROPERTIES_FOR_FILE_OR_FOLDER); - // Parent node already exist - boolean skipWrite = false; - - if (parentPath.isRoot()) { - if (!source.getRepositoryRootUrl().equals(getName())) { - newChildPath = newName; - } else { - newChildPath = "/" + newName; - } - } else { - newChildPath = newPath.getString(registry); - if (!source.getRepositoryRootUrl().equals(getName())) { - newChildPath = newChildPath.substring(1); - } - } - - // check if the new name already exist - try { - if (SvnRepositoryUtil.exists(workspaceRoot, newChildPath)) { - if (conflictBehavior.equals(NodeConflictBehavior.APPEND)) { - throw new InvalidRequestException(SvnRepositoryConnectorI18n.sameNameSiblingsAreNotAllowed.text()); - } else if (conflictBehavior.equals(NodeConflictBehavior.DO_NOT_REPLACE)) { - skipWrite = true; - } - } - } catch (SVNException e1) { - throw new RepositorySourceException(getSourceName(), e1.getMessage()); - } - - // Don't try to write if the node conflict behavior is DO_NOT_REPLACE - if (!skipWrite) { - // create a new, empty file - if (newChildPath != null) { - try { - String rootPath = null; - if (parentPath.isRoot()) { - rootPath = ""; - } else { - rootPath = parentPathAsString; - } - newFile(rootPath, newName, EMPTY_BYTE_ARRAY, null, getName(), workspaceRoot); - } catch (SVNException e) { - I18n msg = SvnRepositoryConnectorI18n.couldNotCreateFile; - throw new RepositorySourceException(getSourceName(), msg.text(parentPathAsString, - getName(), - getSourceName(), - e.getMessage()), e); - } - } - } - } else if (JcrNtLexicon.RESOURCE.equals(primaryType) || ModeShapeLexicon.RESOURCE.equals(primaryType)) { // Resource - ensureValidProperties(context, properties.values(), ALLOWABLE_PROPERTIES_FOR_CONTENT); - if (parentPath.isRoot()) { - newChildPath = parentPathAsString; - if (!source.getRepositoryRootUrl().equals(getName())) { - newChildPath = parentPathAsString.substring(1); - } - } else { - newChildPath = parentPathAsString; - if (!source.getRepositoryRootUrl().equals(getName())) { - newChildPath = newChildPath.substring(1); - } - } - - if (!JcrLexicon.CONTENT.equals(name)) { - I18n msg = SvnRepositoryConnectorI18n.invalidNameForResource; - throw new RepositorySourceException(getSourceName(), msg.text(parentPathAsString, - getName(), - getSourceName(), - newName)); - } - - Property parentPrimaryType = parentNode.getProperty(JcrLexicon.PRIMARY_TYPE); - Name parentPrimaryTypeName = parentPrimaryType == null ? null : nameFactory.create(parentPrimaryType.getFirstValue()); - if (!JcrNtLexicon.FILE.equals(parentPrimaryTypeName)) { - I18n msg = SvnRepositoryConnectorI18n.invalidPathForResource; - throw new RepositorySourceException(getSourceName(), msg.text(parentPathAsString, getName(), getSourceName())); - } - - boolean updateFileContent = true; - switch (conflictBehavior) { - case APPEND: - case REPLACE: - case UPDATE: - // When the "nt:file" parent node was created, it automatically creates the "jcr:content" - // child node with empty content. Therefore, creating a new "jcr:content" node is - // not technically possible (recall that same-name-siblings are not supported in general, - // but certainly not for the "jcr:content" node). Therefore, we can treat all these - // conflict behavior cases as a simple update to the existing "jcr:content" child node. - break; - case DO_NOT_REPLACE: - // TODO check if the file already has content - updateFileContent = false; - } - - Property dataProperty = properties.get(JcrLexicon.DATA); - if (dataProperty == null) { - updateFileContent = false; // no content to write, so just continue - } - if (updateFileContent) { - BinaryFactory binaryFactory = context.getValueFactories().getBinaryFactory(); - Binary binary = binaryFactory.create(properties.get(JcrLexicon.DATA).getFirstValue()); - // get old data - ByteArrayOutputStream contents = new ByteArrayOutputStream(); - SVNProperties svnProperties = new SVNProperties(); - try { - workspaceRoot.getFile(newChildPath, -1, svnProperties, contents); - byte[] oldData = contents.toByteArray(); - - // modify the empty old data with the new resource - if (oldData != null) { - String pathToFile; - if (parentPath.isRoot()) { - pathToFile = ""; - } else { - pathToFile = parentPath.getParent().getString(registry); - } - String fileName = parentPath.getLastSegment().getString(registry); - - modifyFile(pathToFile, fileName, oldData, binary.getBytes(), null, getName(), workspaceRoot); - } - } catch (SVNException e) { - I18n msg = SvnRepositoryConnectorI18n.couldNotReadData; - throw new RepositorySourceException(getSourceName(), msg.text(parentPathAsString, - getName(), - getSourceName(), - e.getMessage()), e); - } - } - - } else if (JcrNtLexicon.FOLDER.equals(primaryType) || primaryType == null) { // Folder - ensureValidProperties(context, properties.values(), ALLOWABLE_PROPERTIES_FOR_FILE_OR_FOLDER); - try { - mkdir(parentPathAsString, newName, null, getName(), workspaceRoot); - } catch (SVNException e) { - I18n msg = SvnRepositoryConnectorI18n.couldNotCreateFile; - throw new RepositorySourceException(getSourceName(), msg.text(parentPathAsString, - getName(), - getSourceName(), - e.getMessage()), e); - } - } else { - I18n msg = SvnRepositoryConnectorI18n.unsupportedPrimaryType; - throw new RepositorySourceException(getSourceName(), msg.text(primaryType.getString(registry), - parentPathAsString, - getName(), - getSourceName())); - } - - PathNode node = getNode(newPath); - - List newChildren = new ArrayList(parentNode.getChildSegments().size() + 1); - newChildren.addAll(parentNode.getChildSegments()); - newChildren.add(node.getPath().getLastSegment()); - - WorkspaceCache cache = getCache(getName()); - cache.set(new DefaultPathNode(parentNode.getPath(), parentNode.getUuid(), parentNode.getProperties(), newChildren)); - cache.set(node); - - return node; - } - - /** - * Create a directory . - * - * @param rootDirPath - the root directory where the created directory will reside - * @param childDirPath - the name of the created directory. - * @param comment - comment for the creation. - * @param inWorkspace - * @param currentRepository - * @throws SVNException - if during the creation, there is an error. - */ - private void mkdir( String rootDirPath, - String childDirPath, - String comment, - String inWorkspace, - SVNRepository currentRepository ) throws SVNException { - - String tempParentPath = rootDirPath; - if (!source.getRepositoryRootUrl().equals(inWorkspace)) { - if (!tempParentPath.equals("/") && tempParentPath.startsWith("/")) { - tempParentPath = tempParentPath.substring(1); - } else if (tempParentPath.equals("/")) { - tempParentPath = ""; - } - } - String checkPath = tempParentPath.length() == 0 ? childDirPath : tempParentPath + "/" + childDirPath; - SVNNodeKind nodeKind = null; - try { - nodeKind = currentRepository.checkPath(checkPath, -1); - } catch (SVNException e) { - SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.UNKNOWN, - "May be a Connecting problem to the repository or a user's authentication failure: {0}", - e.getMessage()); - throw new SVNException(err); - } - - if (nodeKind != null && nodeKind == SVNNodeKind.NONE) { - ScmAction addNodeAction = new AddDirectory(rootDirPath, childDirPath); - SvnActionExecutor executor = new SvnActionExecutor(currentRepository); - comment = comment == null ? "Create a new file " + childDirPath : comment; - executor.execute(addNodeAction, comment); - } else { - SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.UNKNOWN, - "Node with name '{0}' can't be created", - childDirPath); - throw new SVNException(err); - } - } - - /** - * Create a file. - * - * @param rootDirPath - * @param childFilePath - * @param content - * @param comment - * @param inWorkspace - * @param currentRepository - * @throws SVNException - */ - private void newFile( String rootDirPath, - String childFilePath, - byte[] content, - String comment, - String inWorkspace, - SVNRepository currentRepository ) throws SVNException { - - String tempParentPath = rootDirPath; - if (!source.getRepositoryRootUrl().equals(inWorkspace)) { - if (!tempParentPath.equals("/") && tempParentPath.startsWith("/")) { - tempParentPath = tempParentPath.substring(1); - } - } - String checkPath = tempParentPath + "/" + childFilePath; - SVNNodeKind nodeKind = null; - try { - nodeKind = currentRepository.checkPath(checkPath, -1); - } catch (SVNException e) { - SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.UNKNOWN, - "May be a Connecting problem to the repository or a user's authentication failure: {0}", - e.getMessage()); - throw new SVNException(err); - } + class SvnTransaction extends PathTransaction { - if (nodeKind != null && nodeKind == SVNNodeKind.NONE) { - ScmAction addFileNodeAction = new AddFile(rootDirPath, childFilePath, content); - SvnActionExecutor executor = new SvnActionExecutor(currentRepository); - comment = comment == null ? "Create a new file " + childFilePath : comment; - executor.execute(addFileNodeAction, comment); - } else { - SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.UNKNOWN, - "Item with name '{0}' can't be created (already exist)", - childFilePath); - throw new SVNException(err); - } + public SvnTransaction() { + super(SvnRepository.this, source.getRootNodeUuidObject()); } - /** - * Modify a file - * - * @param rootPath - * @param fileName - * @param oldData - * @param newData - * @param comment - * @param inWorkspace - * @param currentRepository - * @throws SVNException - */ - private void modifyFile( String rootPath, - String fileName, - byte[] oldData, - byte[] newData, - String comment, - String inWorkspace, - SVNRepository currentRepository ) throws SVNException { - assert rootPath != null; - assert fileName != null; - assert oldData != null; - assert inWorkspace != null; - assert currentRepository != null; - - try { - - if (!source.getRepositoryRootUrl().equals(inWorkspace)) { - if (rootPath.equals("/")) { - rootPath = ""; - } else { - rootPath = rootPath.substring(1) + "/"; - } - } else { - if (!rootPath.equals("/")) { - rootPath = rootPath + "/"; - } - } - String path = rootPath + fileName; - - SVNNodeKind nodeKind = currentRepository.checkPath(path, -1); - if (nodeKind == SVNNodeKind.NONE || nodeKind == SVNNodeKind.UNKNOWN) { - SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.ENTRY_NOT_FOUND, - "Item with name '{0}' can't be found", - path); - throw new SVNException(err); - } - - ScmAction modifyFileAction = new UpdateFile(rootPath, fileName, oldData, newData); - SvnActionExecutor executor = new SvnActionExecutor(currentRepository); - comment = comment == null ? "modify the " + fileName : comment; - executor.execute(modifyFileAction, comment); - - } catch (SVNException e) { - SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.UNKNOWN, "This error is appeared: " + e.getMessage()); - throw new SVNException(err, e); - } - + @Override + protected PathNode createNode( Segment name, + Path parentPath, + Iterable properties ) { + return new PathNode(null, parentPath, name, properties, new LinkedList()); } - /** - * Delete entry from the repository - * - * @param path - * @param comment - * @param inWorkspace - * @param currentRepository - * @throws SVNException - */ - private void eraseEntry( String path, - String comment, - String inWorkspace, - SVNRepository currentRepository ) throws SVNException { - assert path != null; - assert inWorkspace != null; - if (path.equals("/") || path.equals("")) { - SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.BAD_URL, "The root directory cannot be deleted"); - throw new SVNException(err); - } - - try { - ScmAction deleteEntryAction = new DeleteEntry(path); - SvnActionExecutor executor = new SvnActionExecutor(currentRepository); - comment = comment == null ? "Delete the " + path : comment; - executor.execute(deleteEntryAction, comment); - } catch (SVNException e) { - SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.UNKNOWN, - "unknow error during delete action: {0)", - e.getMessage()); - throw new SVNException(err); - } - } - - public boolean removeNode( ExecutionContext context, - Path nodePath ) { - - NamespaceRegistry registry = context.getNamespaceRegistry(); - - boolean isContentNode = !nodePath.isRoot() && JcrLexicon.CONTENT.equals(nodePath.getLastSegment().getName()); - Path actualPath = isContentNode ? nodePath.getParent() : nodePath; - - try { - SVNNodeKind kind = getNodeKind(context, actualPath, source.getRepositoryRootUrl()); - - if (kind == SVNNodeKind.NONE) { - return false; - } - - if (isContentNode) { - String rootPath = actualPath.getParent().getString(registry); - String fileName = actualPath.getLastSegment().getString(registry); - modifyFile(rootPath, fileName, EMPTY_BYTE_ARRAY, EMPTY_BYTE_ARRAY, null, getName(), workspaceRoot); - } else { - eraseEntry(actualPath.getString(registry), null, getName(), workspaceRoot); - } - } catch (SVNException e) { - throw new RepositorySourceException(getSourceName(), - SvnRepositoryConnectorI18n.deleteFailed.text(nodePath, getSourceName())); - } - - getCache(getName()).invalidate(nodePath); - + @Override + public boolean destroyWorkspace( SvnWorkspace workspace ) throws InvalidWorkspaceException { return true; } - public PathNode setProperties( ExecutionContext context, - Path nodePath, - Map properties ) { - PathNode targetNode = getNode(nodePath); - if (targetNode == null) return null; - - /* - * You can't really remove any properties from SVN nodes. - * You can clear the data of a dna:resource though - */ - - NameFactory nameFactory = context.getValueFactories().getNameFactory(); - Property primaryTypeProperty = targetNode.getProperty(JcrLexicon.PRIMARY_TYPE); - Name primaryTypeName = primaryTypeProperty == null ? null : nameFactory.create(primaryTypeProperty.getFirstValue()); - if (ModeShapeLexicon.RESOURCE.equals(primaryTypeName)) { + @Override + public SvnWorkspace getWorkspace( String name, + SvnWorkspace originalToClone ) throws InvalidWorkspaceException { + SvnRepository repository = SvnRepository.this; - for (Map.Entry entry : properties.entrySet()) { - if (JcrLexicon.DATA.equals(entry.getKey())) { - NamespaceRegistry registry = context.getNamespaceRegistry(); - byte[] data; - if (entry.getValue() == null) { - data = EMPTY_BYTE_ARRAY; - } else { - BinaryFactory binaryFactory = context.getValueFactories().getBinaryFactory(); - data = binaryFactory.create(entry.getValue().getFirstValue()).getBytes(); - - } - - try { - Path actualPath = nodePath.getParent(); - modifyFile(actualPath.getParent().getString(registry), - actualPath.getLastSegment().getString(registry), - EMPTY_BYTE_ARRAY, - data, - "", - getName(), - workspaceRoot); - - PathNode node = getNode(nodePath); - getCache(getName()).set(node); - - return node; - } catch (SVNException ex) { - throw new RepositorySourceException(getSourceName(), - SvnRepositoryConnectorI18n.deleteFailed.text(nodePath, - getSourceName()), ex); - } - } - } + if (originalToClone != null) { + return new SvnWorkspace(name, originalToClone, repository.getWorkspaceDirectory(name)); } - - return targetNode; + return new SvnWorkspace(repository, getWorkspaceDirectory(name), name, source.getRootNodeUuidObject()); } - protected boolean readNode( ExecutionContext context, - String workspaceName, - Path requestedPath, - List properties, - List children ) throws SVNException { - PathFactory pathFactory = context.getValueFactories().getPathFactory(); - NamespaceRegistry registry = context.getNamespaceRegistry(); - - if (requestedPath.isRoot()) { - // workspace root must be a directory - if (children != null) { - final Collection entries = SvnRepositoryUtil.getDir(workspaceRoot, ""); - for (SVNDirEntry entry : entries) { - // All of the children of a directory will be another directory or a file, but never a "jcr:content" node - // ... - children.add(pathFactory.createSegment(entry.getName())); - } - } - // There are no properties on the root ... - } else { - // Generate the properties for this File object ... - PropertyFactory factory = context.getPropertyFactory(); - DateTimeFactory dateFactory = context.getValueFactories().getDateFactory(); - - // Figure out the kind of node this represents ... - SVNNodeKind kind = getNodeKind(context, requestedPath, source.getRepositoryRootUrl()); - if (kind == SVNNodeKind.NONE) { - // The node doesn't exist - return false; - } - if (kind == SVNNodeKind.DIR) { - String directoryPath = requestedPath.getString(registry); - if (!source.getRepositoryRootUrl().equals(workspaceName)) { - directoryPath = directoryPath.substring(1); - } - if (children != null) { - // Decide how to represent the children ... - Collection dirEntries = SvnRepositoryUtil.getDir(workspaceRoot, directoryPath); - for (SVNDirEntry entry : dirEntries) { - // All of the children of a directory will be another directory or a file, - // but never a "jcr:content" node ... - children.add(pathFactory.createSegment(entry.getName())); - } - } - if (properties != null) { - // Load the properties for this directory ...... - properties.add(factory.create(JcrLexicon.PRIMARY_TYPE, JcrNtLexicon.FOLDER)); - SVNDirEntry entry = getEntryInfo(workspaceRoot, directoryPath); - if (entry != null) { - properties.add(factory.create(JcrLexicon.CREATED, dateFactory.create(entry.getDate()))); - } - } - } else { - // It's not a directory, so must be a file; the only child of an nt:file is the "jcr:content" node - // ... - if (requestedPath.endsWith(JcrLexicon.CONTENT)) { - // There are never any children of these nodes, just properties ... - if (properties != null) { - String contentPath = requestedPath.getParent().getString(registry); - if (!source.getRepositoryRootUrl().equals(workspaceName)) { - contentPath = contentPath.substring(1); - } - SVNDirEntry entry = getEntryInfo(workspaceRoot, contentPath); - if (entry != null) { - // The request is to get properties of the "jcr:content" child node ... - // Do NOT use "nt:resource", since it extends "mix:referenceable". The JCR spec - // does not require that "jcr:content" is of type "nt:resource", but rather just - // suggests it. Therefore, we can use "dna:resource", which is identical to - // "nt:resource" except it does not extend "mix:referenceable" - properties.add(factory.create(JcrLexicon.PRIMARY_TYPE, ModeShapeLexicon.RESOURCE)); - properties.add(factory.create(JcrLexicon.LAST_MODIFIED, dateFactory.create(entry.getDate()))); - } - - ByteArrayOutputStream os = new ByteArrayOutputStream(); - SVNProperties fileProperties = new SVNProperties(); - getData(contentPath, fileProperties, os); - String mimeType = fileProperties.getStringValue(SVNProperty.MIME_TYPE); - if (mimeType == null) mimeType = DEFAULT_MIME_TYPE; - properties.add(factory.create(JcrLexicon.MIMETYPE, mimeType)); - - if (os.toByteArray().length > 0) { - // Now put the file's content into the "jcr:data" property ... - BinaryFactory binaryFactory = context.getValueFactories().getBinaryFactory(); - properties.add(factory.create(JcrLexicon.DATA, binaryFactory.create(os.toByteArray()))); - } - } - } else { - // Determine the corresponding file path for this object ... - String filePath = requestedPath.getString(registry); - if (!source.getRepositoryRootUrl().equals(workspaceName)) { - filePath = filePath.substring(1); - } - if (children != null) { - // Not a "jcr:content" child node but rather an nt:file node, so add the child ... - children.add(pathFactory.createSegment(JcrLexicon.CONTENT)); - } - if (properties != null) { - // Now add the properties to "nt:file" ... - properties.add(factory.create(JcrLexicon.PRIMARY_TYPE, JcrNtLexicon.FILE)); - ByteArrayOutputStream os = new ByteArrayOutputStream(); - SVNProperties fileProperties = new SVNProperties(); - getData(filePath, fileProperties, os); - String created = fileProperties.getStringValue(SVNProperty.COMMITTED_DATE); - properties.add(factory.create(JcrLexicon.CREATED, dateFactory.create(created))); - } - } - } - } - return true; - } - - /** - * Get some important informations of a path - * - * @param repos - * @param path - the path - * @return - the {@link SVNDirEntry}, or null if there is no such entry - */ - protected SVNDirEntry getEntryInfo( SVNRepository repos, - String path ) { - assert path != null; - SVNDirEntry entry = null; - try { - entry = repos.info(path, -1); - } catch (SVNException e) { - throw new RepositorySourceException( - getSourceName(), - SvnRepositoryConnectorI18n.connectingFailureOrUserAuthenticationProblem.text(getSourceName())); - } - return entry; + @Override + protected void validateNode( SvnWorkspace workspace, + PathNode node ) { + workspace.validate(node); } + } - /** - * Get the content of a file. - * - * @param path - the path to that file. - * @param properties - the properties of the file. - * @param os - the output stream where to store the content. - * @throws SVNException - throws if such path is not at that revision or in case of a connection problem. - */ - protected void getData( String path, - SVNProperties properties, - OutputStream os ) throws SVNException { - workspaceRoot.getFile(path, -1, properties, os); + protected SVNRepository getWorkspaceDirectory( String workspaceName ) { + if (workspaceName == null) workspaceName = source().getDefaultWorkspaceName(); + if (source().getRepositoryRootUrl().endsWith("/")) { + workspaceName = source().getRepositoryRootUrl() + workspaceName; + } else { + workspaceName = source().getRepositoryRootUrl() + "/" + workspaceName; } - protected SVNNodeKind getNodeKind( ExecutionContext context, - Path path, - String repositoryRootUrl ) throws SVNException { - assert path != null; - assert repositoryRootUrl != null; - - // See if the path is a "jcr:content" node ... - if (path.endsWith(JcrLexicon.CONTENT)) { - // We only want to use the parent path to find the actual file ... - path = path.getParent(); - } - String pathAsString = path.getString(context.getNamespaceRegistry()); - if (!repositoryRootUrl.equals(getName())) { - pathAsString = pathAsString.substring(1); - } - - String absolutePath = pathAsString; - SVNNodeKind kind = workspaceRoot.checkPath(absolutePath, -1); - if (kind == SVNNodeKind.UNKNOWN) { - // node is unknown - throw new RepositorySourceException(getSourceName(), - SvnRepositoryConnectorI18n.nodeIsActuallyUnknow.text(pathAsString)); - } - return kind; + SVNRepository repository = null; + SVNRepository repos = SvnRepositoryUtil.createRepository(workspaceName, source().getUsername(), source().getPassword()); + if (SvnRepositoryUtil.isDirectory(repos, "")) { + repository = repos; + } else { + return null; } + return repository; + } - protected SVNRepository getWorkspaceDirectory( String workspaceName ) { - if (workspaceName == null) workspaceName = source.getDefaultWorkspaceName(); - - if (source.getRepositoryRootUrl().endsWith("/")) { - workspaceName = source.getRepositoryRootUrl() + workspaceName; - } else { - workspaceName = source.getRepositoryRootUrl() + "/" + workspaceName; - } + /** + * Custom {@link Processor} for the file system connector. This processor throws accurate exceptions on attempts to reorder + * nodes, since the file system connector does not support node ordering. Otherwise, it provides default behavior. + */ + class SvnProcessor extends Processor { - SVNRepository repository = null; - SVNRepository repos = SvnRepositoryUtil.createRepository(workspaceName, source.getUsername(), source.getPassword()); - if (SvnRepositoryUtil.isDirectory(repos, "")) { - repository = repos; - } else { - return null; - } - return repository; + public SvnProcessor( Transaction txn, + Repository repository, + Observer observer, + boolean updatesAllowed ) { + super(txn, repository, observer, updatesAllowed); } - /** - * Checks that the collection of {@code properties} only contains properties with allowable names. - * - * @param context - * @param properties - * @param validPropertyNames - * @throws RepositorySourceException if {@code properties} contains a - * @see #ALLOWABLE_PROPERTIES_FOR_CONTENT - * @see #ALLOWABLE_PROPERTIES_FOR_FILE_OR_FOLDER - */ - protected void ensureValidProperties( ExecutionContext context, - Collection properties, - Set validPropertyNames ) { - List invalidNames = new LinkedList(); - NamespaceRegistry registry = context.getNamespaceRegistry(); - - for (Property property : properties) { - if (!validPropertyNames.contains(property.getName())) { - invalidNames.add(property.getName().getString(registry)); - } - } - - if (!invalidNames.isEmpty()) { - throw new RepositorySourceException(getSourceName(), - SvnRepositoryConnectorI18n.invalidPropertyNames.text(invalidNames.toString())); + @Override + public void process( MoveBranchRequest request ) { + if (request.before() != null) { + I18n msg = SvnRepositoryConnectorI18n.nodeOrderingNotSupported; + throw new InvalidRequestException(msg.text(source.getName())); } + super.process(request); } } Index: extensions/modeshape-connector-svn/src/main/java/org/modeshape/connector/svn/SvnRepositoryConnectorI18n.java =================================================================== --- extensions/modeshape-connector-svn/src/main/java/org/modeshape/connector/svn/SvnRepositoryConnectorI18n.java (revision 1825) +++ extensions/modeshape-connector-svn/src/main/java/org/modeshape/connector/svn/SvnRepositoryConnectorI18n.java (working copy) @@ -64,6 +64,7 @@ public final class SvnRepositoryConnectorI18n { public static I18n couldNotCreateFile; public static I18n couldNotReadData; public static I18n deleteFailed; + public static I18n nodeOrderingNotSupported; static { try { Index: extensions/modeshape-connector-svn/src/main/java/org/modeshape/connector/svn/SvnRepositorySource.java =================================================================== --- extensions/modeshape-connector-svn/src/main/java/org/modeshape/connector/svn/SvnRepositorySource.java (revision 1825) +++ extensions/modeshape-connector-svn/src/main/java/org/modeshape/connector/svn/SvnRepositorySource.java (working copy) @@ -35,12 +35,16 @@ import net.jcip.annotations.ThreadSafe; import org.modeshape.common.i18n.I18n; import org.modeshape.common.util.CheckArg; import org.modeshape.common.util.StringUtil; +import org.modeshape.connector.svn.SvnRepository.SvnTransaction; +import org.modeshape.graph.ExecutionContext; import org.modeshape.graph.connector.RepositoryConnection; import org.modeshape.graph.connector.RepositorySource; import org.modeshape.graph.connector.RepositorySourceCapabilities; import org.modeshape.graph.connector.RepositorySourceException; -import org.modeshape.graph.connector.path.AbstractPathRepositorySource; -import org.modeshape.graph.connector.path.PathRepositoryConnection; +import org.modeshape.graph.connector.base.AbstractRepositorySource; +import org.modeshape.graph.connector.base.Connection; +import org.modeshape.graph.connector.base.PathNode; +import org.modeshape.graph.request.CreateWorkspaceRequest.CreateConflictBehavior; /** * The {@link RepositorySource} for the connector that exposes an area of the local/remote svn repository as content in a @@ -49,7 +53,7 @@ import org.modeshape.graph.connector.path.PathRepositoryConnection; * existing directories. */ @ThreadSafe -public class SvnRepositorySource extends AbstractPathRepositorySource implements ObjectFactory { +public class SvnRepositorySource extends AbstractRepositorySource implements ObjectFactory { /** * The first serialized version of this source. Version {@value} . @@ -102,6 +106,8 @@ public class SvnRepositorySource extends AbstractPathRepositorySource implements private transient SvnRepository repository; + private ExecutionContext defaultContext = new ExecutionContext(); + /** * Create a repository source instance. */ @@ -316,7 +322,7 @@ public class SvnRepositorySource extends AbstractPathRepositorySource implements ref.add(new StringRefAddr(SVN_PASSWORD, getPassword())); } ref.add(new StringRefAddr(RETRY_LIMIT, Integer.toString(getRetryLimit()))); - ref.add(new StringRefAddr(ROOT_NODE_UUID, rootNodeUuid.toString())); + ref.add(new StringRefAddr(ROOT_NODE_UUID, getRootNodeUuidObject().toString())); ref.add(new StringRefAddr(DEFAULT_WORKSPACE, getDefaultWorkspaceName())); ref.add(new StringRefAddr(ALLOW_CREATING_WORKSPACES, Boolean.toString(isCreatingWorkspacesAllowed()))); String[] workspaceNames = getPredefinedWorkspaceNames(); @@ -363,7 +369,7 @@ public class SvnRepositorySource extends AbstractPathRepositorySource implements if (username != null) source.setUsername(username); if (password != null) source.setPassword(password); if (retryLimit != null) source.setRetryLimit(Integer.parseInt(retryLimit)); - if (rootNodeUuid != null) source.setRootNodeUuid(rootNodeUuid); + if (rootNodeUuid != null) source.setRootNodeUuidObject(rootNodeUuid); if (defaultWorkspace != null) source.setDefaultWorkspaceName(defaultWorkspace); if (createWorkspaces != null) source.setCreatingWorkspacesAllowed(Boolean.parseBoolean(createWorkspaces)); if (workspaceNames != null && workspaceNames.length != 0) source.setPredefinedWorkspaceNames(workspaceNames); @@ -403,10 +409,21 @@ public class SvnRepositorySource extends AbstractPathRepositorySource implements repositoryRootURL = repositoryRootURL.trim(); if (repositoryRootURL.endsWith("/")) repositoryRootURL = repositoryRootURL + "/"; - if (this.repository == null) { - this.repository = new SvnRepository(this); - } + if (repository == null) { + repository = new SvnRepository(this); + + ExecutionContext context = repositoryContext != null ? repositoryContext.getExecutionContext() : defaultContext; + SvnTransaction txn = repository.startTransaction(context, false); + try { + // Create the set of initial workspaces ... + for (String initialName : getPredefinedWorkspaceNames()) { + repository.createWorkspace(txn, initialName, CreateConflictBehavior.DO_NOT_CREATE, null); + } + } finally { + txn.commit(); + } - return new PathRepositoryConnection(this, this.repository); + } + return new Connection(this, repository); } } Index: extensions/modeshape-connector-svn/src/main/java/org/modeshape/connector/svn/SvnWorkspace.java new file mode 100644 =================================================================== --- /dev/null (revision 1825) +++ extensions/modeshape-connector-svn/src/main/java/org/modeshape/connector/svn/SvnWorkspace.java (working copy) @@ -0,0 +1,561 @@ +package org.modeshape.connector.svn; + +import java.io.ByteArrayOutputStream; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Set; +import java.util.UUID; +import org.modeshape.common.i18n.I18n; +import org.modeshape.connector.scm.ScmAction; +import org.modeshape.connector.svn.mgnt.AddDirectory; +import org.modeshape.connector.svn.mgnt.AddFile; +import org.modeshape.connector.svn.mgnt.DeleteEntry; +import org.modeshape.connector.svn.mgnt.UpdateFile; +import org.modeshape.graph.ExecutionContext; +import org.modeshape.graph.JcrLexicon; +import org.modeshape.graph.JcrNtLexicon; +import org.modeshape.graph.ModeShapeIntLexicon; +import org.modeshape.graph.ModeShapeLexicon; +import org.modeshape.graph.connector.RepositorySourceException; +import org.modeshape.graph.connector.base.PathNode; +import org.modeshape.graph.connector.base.PathWorkspace; +import org.modeshape.graph.property.Binary; +import org.modeshape.graph.property.BinaryFactory; +import org.modeshape.graph.property.DateTimeFactory; +import org.modeshape.graph.property.Name; +import org.modeshape.graph.property.NameFactory; +import org.modeshape.graph.property.NamespaceRegistry; +import org.modeshape.graph.property.Path; +import org.modeshape.graph.property.PathFactory; +import org.modeshape.graph.property.Property; +import org.modeshape.graph.property.PropertyFactory; +import org.modeshape.graph.property.Path.Segment; +import org.tmatesoft.svn.core.SVNCommitInfo; +import org.tmatesoft.svn.core.SVNDirEntry; +import org.tmatesoft.svn.core.SVNException; +import org.tmatesoft.svn.core.SVNNodeKind; +import org.tmatesoft.svn.core.SVNProperties; +import org.tmatesoft.svn.core.SVNProperty; +import org.tmatesoft.svn.core.auth.ISVNAuthenticationManager; +import org.tmatesoft.svn.core.io.ISVNEditor; +import org.tmatesoft.svn.core.io.SVNRepository; +import org.tmatesoft.svn.core.wc.SVNWCUtil; + +/** + * Workspace implementation for SVN repository connector + */ +public class SvnWorkspace extends PathWorkspace { + private static final String DEFAULT_MIME_TYPE = "application/octet-stream"; + protected static final byte[] EMPTY_BYTE_ARRAY = new byte[0]; + + private final Set ALLOWABLE_PRIMARY_TYPES = Collections.unmodifiableSet(new HashSet(Arrays.asList(new Name[] { + JcrNtLexicon.FOLDER, JcrNtLexicon.FILE, JcrNtLexicon.RESOURCE, ModeShapeLexicon.RESOURCE, null}))); + + /** + * Only certain properties are tolerated when writing content (dna:resource or jcr:resource) nodes. These properties are + * implicitly stored (primary type, data) or silently ignored (encoded, mimetype, last modified). The silently ignored + * properties must be accepted to stay compatible with the JCR specification. + */ + private final Set ALLOWABLE_PROPERTIES_FOR_CONTENT = Collections.unmodifiableSet(new HashSet( + Arrays.asList(new Name[] { + JcrLexicon.PRIMARY_TYPE, + JcrLexicon.DATA, + JcrLexicon.ENCODED, + JcrLexicon.MIMETYPE, + JcrLexicon.LAST_MODIFIED, + JcrLexicon.UUID, + ModeShapeIntLexicon.NODE_DEFINITON}))); + /** + * Only certain properties are tolerated when writing files (nt:file) or folders (nt:folder) nodes. These properties are + * implicitly stored in the file or folder (primary type, created). + */ + private final Set ALLOWABLE_PROPERTIES_FOR_FILE_OR_FOLDER = Collections.unmodifiableSet(new HashSet( + Arrays.asList(new Name[] { + JcrLexicon.PRIMARY_TYPE, + JcrLexicon.CREATED, + JcrLexicon.UUID, + ModeShapeIntLexicon.NODE_DEFINITON}))); + + // The SvnRepository is a reference to the ModeShape repository class + private final SvnRepository repository; + + // The SVNRepository is a reference to the tmatesoft SVN repository class + private final SVNRepository workspaceRoot; + + public SvnWorkspace( SvnRepository repository, + SVNRepository workspaceRoot, + String name, + UUID rootNodeUuid ) { + super(name, rootNodeUuid); + + this.repository = repository; + this.workspaceRoot = workspaceRoot; + + ISVNAuthenticationManager authManager = SVNWCUtil.createDefaultAuthenticationManager(repository.source().getUsername(), + repository.source().getPassword()); + workspaceRoot.setAuthenticationManager(authManager); + } + + public SvnWorkspace( String name, + SvnWorkspace originalToClone, + SVNRepository workspaceRoot ) { + super(name, originalToClone.getRootNodeUuid()); + + this.repository = originalToClone.repository; + this.workspaceRoot = workspaceRoot; + + cloneWorkspace(originalToClone); + } + + private void cloneWorkspace( SvnWorkspace original ) { + I18n msg = SvnRepositoryConnectorI18n.sourceDoesNotSupportCloningWorkspaces; + throw new UnsupportedOperationException(msg.text(original.source().getName())); + } + + private final SvnRepositorySource source() { + return repository.source(); + } + + private final String getSourceName() { + return source().getName(); + } + + private final ExecutionContext context() { + return source().getRepositoryContext().getExecutionContext(); + } + + private final NameFactory nameFactory() { + return context().getValueFactories().getNameFactory(); + } + + private final PathFactory pathFactory() { + return context().getValueFactories().getPathFactory(); + } + + private final Path pathTo( PathNode node ) { + if (node.getParent() == null) { + return pathFactory().createRootPath(); + } + return pathFactory().create(node.getParent(), node.getName()); + } + + @Override + public PathNode getRootNode() { + return getNode(context().getValueFactories().getPathFactory().createRootPath()); + } + + @Override + public PathNode getNode( Path path ) { + PathNode node; + + ExecutionContext context = source().getRepositoryContext().getExecutionContext(); + List properties = new LinkedList(); + List children = new LinkedList(); + + try { + boolean result = readNode(context, this.getName(), path, properties, children); + if (!result) return null; + } catch (SVNException ex) { + return null; + } + + UUID uuid = path.isRoot() ? source().getRootNodeUuidObject() : null; + Path parent = path.isRoot() ? null : path.getParent(); + Segment name = path.isRoot() ? null : path.getLastSegment(); + + node = new PathNode(uuid, parent, name, properties, children); + + return node; + } + + protected boolean readNode( ExecutionContext context, + String workspaceName, + Path requestedPath, + List properties, + List children ) throws SVNException { + PathFactory pathFactory = context.getValueFactories().getPathFactory(); + NamespaceRegistry registry = context.getNamespaceRegistry(); + + if (requestedPath.isRoot()) { + // workspace root must be a directory + if (children != null) { + final Collection entries = SvnRepositoryUtil.getDir(workspaceRoot, ""); + for (SVNDirEntry entry : entries) { + // All of the children of a directory will be another directory or a file, but never a "jcr:content" node + // ... + children.add(pathFactory.createSegment(entry.getName())); + } + } + // There are no properties on the root ... + } else { + // Generate the properties for this File object ... + PropertyFactory factory = context.getPropertyFactory(); + DateTimeFactory dateFactory = context.getValueFactories().getDateFactory(); + + // Figure out the kind of node this represents ... + SVNNodeKind kind = getNodeKind(context, requestedPath, source().getRepositoryRootUrl()); + if (kind == SVNNodeKind.NONE) { + // The node doesn't exist + return false; + } + if (kind == SVNNodeKind.DIR) { + String directoryPath = requestedPath.getString(registry); + if (!source().getRepositoryRootUrl().equals(workspaceName)) { + directoryPath = directoryPath.substring(1); + } + if (children != null) { + // Decide how to represent the children ... + Collection dirEntries = SvnRepositoryUtil.getDir(workspaceRoot, directoryPath); + for (SVNDirEntry entry : dirEntries) { + // All of the children of a directory will be another directory or a file, + // but never a "jcr:content" node ... + children.add(pathFactory.createSegment(entry.getName())); + } + } + if (properties != null) { + // Load the properties for this directory ...... + properties.add(factory.create(JcrLexicon.PRIMARY_TYPE, JcrNtLexicon.FOLDER)); + // SVNDirEntry entry = getEntryInfo(workspaceRoot, directoryPath); + SVNDirEntry entry = workspaceRoot.info(directoryPath, -1); + if (entry != null) { + properties.add(factory.create(JcrLexicon.CREATED, dateFactory.create(entry.getDate()))); + } + } + } else { + // It's not a directory, so must be a file; the only child of an nt:file is the "jcr:content" node + // ... + if (requestedPath.endsWith(JcrLexicon.CONTENT)) { + // There are never any children of these nodes, just properties ... + if (properties != null) { + String contentPath = requestedPath.getParent().getString(registry); + if (!source().getRepositoryRootUrl().equals(workspaceName)) { + contentPath = contentPath.substring(1); + } + SVNDirEntry entry = workspaceRoot.info(contentPath, -1); + if (entry != null) { + // The request is to get properties of the "jcr:content" child node ... + // Do NOT use "nt:resource", since it extends "mix:referenceable". The JCR spec + // does not require that "jcr:content" is of type "nt:resource", but rather just + // suggests it. Therefore, we can use "dna:resource", which is identical to + // "nt:resource" except it does not extend "mix:referenceable" + properties.add(factory.create(JcrLexicon.PRIMARY_TYPE, ModeShapeLexicon.RESOURCE)); + properties.add(factory.create(JcrLexicon.LAST_MODIFIED, dateFactory.create(entry.getDate()))); + } + + ByteArrayOutputStream os = new ByteArrayOutputStream(); + SVNProperties fileProperties = new SVNProperties(); + workspaceRoot.getFile(contentPath, -1, fileProperties, os); + String mimeType = fileProperties.getStringValue(SVNProperty.MIME_TYPE); + if (mimeType == null) mimeType = DEFAULT_MIME_TYPE; + properties.add(factory.create(JcrLexicon.MIMETYPE, mimeType)); + + if (os.toByteArray().length > 0) { + // Now put the file's content into the "jcr:data" property ... + BinaryFactory binaryFactory = context.getValueFactories().getBinaryFactory(); + properties.add(factory.create(JcrLexicon.DATA, binaryFactory.create(os.toByteArray()))); + } + } + } else { + // Determine the corresponding file path for this object ... + String filePath = requestedPath.getString(registry); + if (!source().getRepositoryRootUrl().equals(workspaceName)) { + filePath = filePath.substring(1); + } + if (children != null) { + // Not a "jcr:content" child node but rather an nt:file node, so add the child ... + children.add(pathFactory.createSegment(JcrLexicon.CONTENT)); + } + if (properties != null) { + // Now add the properties to "nt:file" ... + properties.add(factory.create(JcrLexicon.PRIMARY_TYPE, JcrNtLexicon.FILE)); + ByteArrayOutputStream os = new ByteArrayOutputStream(); + SVNProperties fileProperties = new SVNProperties(); + workspaceRoot.getFile(filePath, -1, fileProperties, os); + String created = fileProperties.getStringValue(SVNProperty.COMMITTED_DATE); + properties.add(factory.create(JcrLexicon.CREATED, dateFactory.create(created))); + } + } + } + } + return true; + } + + protected SVNNodeKind getNodeKind( ExecutionContext context, + Path path, + String repositoryRootUrl ) throws SVNException { + assert path != null; + assert repositoryRootUrl != null; + + // See if the path is a "jcr:content" node ... + if (path.endsWith(JcrLexicon.CONTENT)) { + // We only want to use the parent path to find the actual file ... + path = path.getParent(); + } + String pathAsString = path.getString(context.getNamespaceRegistry()); + if (!repositoryRootUrl.equals(getName())) { + pathAsString = pathAsString.substring(1); + } + + String absolutePath = pathAsString; + SVNNodeKind kind = workspaceRoot.checkPath(absolutePath, -1); + if (kind == SVNNodeKind.UNKNOWN) { + // node is unknown + throw new RepositorySourceException(getSourceName(), + SvnRepositoryConnectorI18n.nodeIsActuallyUnknow.text(pathAsString)); + } + return kind; + } + + private Name primaryTypeFor( PathNode node ) { + Property primaryTypeProp = node.getProperty(JcrLexicon.PRIMARY_TYPE); + Name primaryType = primaryTypeProp == null ? null : nameFactory().create(primaryTypeProp.getFirstValue()); + + return primaryType; + } + + protected void validate( PathNode node ) { + Name primaryType = primaryTypeFor(node); + + if (!ALLOWABLE_PRIMARY_TYPES.contains(primaryType)) { + I18n msg = SvnRepositoryConnectorI18n.unsupportedPrimaryType; + NamespaceRegistry registry = context().getNamespaceRegistry(); + String path = pathTo(node).getString(registry); + String primaryTypeName = primaryType.getString(registry); + throw new RepositorySourceException(getSourceName(), msg.text(path, getName(), getSourceName(), primaryTypeName)); + } + + Set invalidPropertyNames = new HashSet(node.getProperties().keySet()); + if (JcrNtLexicon.RESOURCE.equals(primaryType) || ModeShapeLexicon.RESOURCE.equals(primaryType)) { + invalidPropertyNames.removeAll(ALLOWABLE_PROPERTIES_FOR_CONTENT); + } else { + invalidPropertyNames.removeAll(ALLOWABLE_PROPERTIES_FOR_FILE_OR_FOLDER); + } + + if (!invalidPropertyNames.isEmpty()) { + I18n msg = SvnRepositoryConnectorI18n.invalidPropertyNames; + throw new RepositorySourceException(getSourceName(), msg.text(invalidPropertyNames)); + + } + + } + + @Override + public ChangeCommand createMoveCommand( PathNode source, + PathNode target ) { + // Manually create all of the commands needed to delete the source and recreate it in the target + List commands = new LinkedList(); + LinkedList pathsToCopy = new LinkedList(); + + Path sourceRoot = pathTo(source); + Path targetRoot = pathTo(target); + + pathsToCopy.add(sourceRoot); + + while (!pathsToCopy.isEmpty()) { + Path path = pathsToCopy.removeFirst(); + PathNode node = getNode(path); + + assert node != null : path; + + + Path oldParent = node.getParent(); + Path newParent = oldParent.relativeTo(sourceRoot).resolveAgainst(targetRoot); + + PathNode newNode = node.clone().withParent(newParent); + if (path.equals(sourceRoot)) { + newNode = newNode.withName(target.getName()); + } + commands.add(createPutCommand(null, newNode)); + + for (Segment child : node.getChildren()) { + pathsToCopy.add(pathFactory().create(path, child)); + } + + } + + commands.add(createRemoveCommand(pathTo(source))); + return new SvnCompositeCommand(commands); + } + + @Override + public SvnCommand createPutCommand( PathNode previousNode, + PathNode node ) { + Name primaryType = primaryTypeFor(node); + + // Can't modify the root node + if (node.getParent() == null) { + return null; + } + + NamespaceRegistry registry = context().getNamespaceRegistry(); + String parentPath = node.getParent().getString(registry); + String name = node.getName().getString(registry); + + if (primaryType == null || JcrNtLexicon.FOLDER.equals(primaryType)) { + if (previousNode != null) { + return null; + } + return new SvnPutFolderCommand(parentPath, name); + } + + if (JcrNtLexicon.FILE.equals(primaryType)) { + if (previousNode != null) { + return null; + } + return new SvnPutFileCommand(parentPath, name, EMPTY_BYTE_ARRAY); + } + + byte[] oldContent; + + if (previousNode != null) { + Property oldContentProp = previousNode.getProperty(JcrLexicon.DATA); + Binary oldContentBin = oldContentProp == null ? null : context().getValueFactories().getBinaryFactory().create(oldContentProp.getFirstValue()); + oldContent = oldContentBin == null ? EMPTY_BYTE_ARRAY : oldContentBin.getBytes(); + } else { + oldContent = EMPTY_BYTE_ARRAY; + } + + Property contentProp = node.getProperty(JcrLexicon.DATA); + Binary contentBin = contentProp == null ? null : context().getValueFactories().getBinaryFactory().create(contentProp.getFirstValue()); + byte[] newContent = contentBin == null ? EMPTY_BYTE_ARRAY : contentBin.getBytes(); + + // The path for a content node ends with the /jcr:content. Need to go up one level to get the file name. + Path filePath = node.getParent(); + String fileDir = filePath.isRoot() ? "/" : filePath.getParent().getString(registry); + String fileName = filePath.getLastSegment().getString(registry); + + return new SvnPutContentCommand(fileDir, fileName, oldContent, newContent); + } + + @Override + public SvnCommand createRemoveCommand( Path path ) { + String svnPath = path.getString(context().getNamespaceRegistry()); + return new SvnRemoveCommand(svnPath); + } + + @Override + public void commit( List> commands ) { + ISVNEditor editor = null; + boolean commit = true; + + try { + editor = workspaceRoot.getCommitEditor("ModeShape commit", null); + editor.openRoot(-1); + + for (ChangeCommand command : commands) { + if (command == null) continue; + SvnCommand svnCommand = (SvnCommand)command; + svnCommand.setEditor(editor); + svnCommand.apply(); + } + } catch (SVNException ex) { + commit = false; + throw new IllegalStateException(ex); + } finally { + if (editor != null) { + try { + editor.closeDir(); + } catch (SVNException ignore) { + + } + } + } + assert editor != null; + if (commit) { + try { + SVNCommitInfo info = editor.closeEdit(); + if (info.getErrorMessage() != null) { + throw new IllegalStateException(info.getErrorMessage().getFullMessage()); + } + } catch (SVNException ex) { + throw new IllegalStateException(ex); + } + } + } + + protected class SvnCommand implements ChangeCommand { + protected ISVNEditor editor; + private final ScmAction action; + + protected SvnCommand( ScmAction action ) { + this.action = action; + } + + public void setEditor( ISVNEditor editor ) { + this.editor = editor; + } + + @Override + public void apply() { + assert editor != null; + try { + action.applyAction(editor); + } catch (Exception ex) { + throw new IllegalStateException(ex); + } + } + + @Override + public String toString() { + return getClass().getSimpleName() + " for " + action.toString(); + } + + } + + protected class SvnPutFileCommand extends SvnCommand { + public SvnPutFileCommand( String parentPath, + String fileName, + byte[] content ) { + super(new AddFile(parentPath, fileName, content)); + } + } + + protected class SvnPutContentCommand extends SvnCommand { + public SvnPutContentCommand( String parentPath, + String fileName, + byte[] oldcontent, + byte[] content ) { + super(new UpdateFile(parentPath, fileName, oldcontent, content)); + } + } + + protected class SvnPutFolderCommand extends SvnCommand { + public SvnPutFolderCommand( String parentPath, + String childPath ) { + super(new AddDirectory(parentPath, childPath)); + } + } + + protected class SvnRemoveCommand extends SvnCommand { + public SvnRemoveCommand( String path ) { + super(new DeleteEntry(path)); + } + } + + protected class SvnCompositeCommand extends SvnCommand { + List commands; + + protected SvnCompositeCommand( List commands ) { + super(null); + + this.commands = commands; + } + + @Override + public void apply() { + for (SvnCommand command : commands) { + command.setEditor(editor); + command.apply(); + } + } + + @Override + public String toString() { + return commands.toString(); + } + } +} Index: extensions/modeshape-connector-svn/src/main/java/org/modeshape/connector/svn/mgnt/AddDirectory.java =================================================================== --- extensions/modeshape-connector-svn/src/main/java/org/modeshape/connector/svn/mgnt/AddDirectory.java (revision 1825) +++ extensions/modeshape-connector-svn/src/main/java/org/modeshape/connector/svn/mgnt/AddDirectory.java (working copy) @@ -57,4 +57,9 @@ public class AddDirectory implements ScmAction { ISVNEditorUtil.closeDirectories(editor, childDirPath); ISVNEditorUtil.closeDirectories(editor, this.rootDirPath); } + + @Override + public String toString() { + return "AddDirectory {" + rootDirPath + "/" + childDirPath + "}"; + } } Index: extensions/modeshape-connector-svn/src/main/java/org/modeshape/connector/svn/mgnt/AddFile.java =================================================================== --- extensions/modeshape-connector-svn/src/main/java/org/modeshape/connector/svn/mgnt/AddFile.java (revision 1825) +++ extensions/modeshape-connector-svn/src/main/java/org/modeshape/connector/svn/mgnt/AddFile.java (working copy) @@ -54,4 +54,9 @@ public class AddFile implements ScmAction { ISVNEditorUtil.closeDirectories(editor, this.path); } + @Override + public String toString() { + return "AddFile {" + path + "/" + file + "}"; + } + } Index: extensions/modeshape-connector-svn/src/main/java/org/modeshape/connector/svn/mgnt/UpdateFile.java =================================================================== --- extensions/modeshape-connector-svn/src/main/java/org/modeshape/connector/svn/mgnt/UpdateFile.java (revision 1825) +++ extensions/modeshape-connector-svn/src/main/java/org/modeshape/connector/svn/mgnt/UpdateFile.java (working copy) @@ -61,4 +61,9 @@ public class UpdateFile implements ScmAction { ISVNEditorUtil.closeDirectories(editor, path); } + @Override + public String toString() { + return "UpdateFile {" + path + "/" + file + "}"; + } + } Index: extensions/modeshape-connector-svn/src/main/resources/org/modeshape/connector/svn/SvnRepositoryConnectorI18n.properties =================================================================== --- extensions/modeshape-connector-svn/src/main/resources/org/modeshape/connector/svn/SvnRepositoryConnectorI18n.properties (revision 1825) +++ extensions/modeshape-connector-svn/src/main/resources/org/modeshape/connector/svn/SvnRepositoryConnectorI18n.properties (working copy) @@ -51,7 +51,7 @@ invalidPropertyNames = Attempt to set or update invalid property names: {0} invalidNameForResource = Invalid node name "{3}" for node at path "{0}" in workspace "{1}" in {2}. The name of nodes with primary type nt:resource or dna:resource must be "jcr:content". invalidPathForResource = Invalid parent type for node at path "{0}" in workspace "{1}" in {2}. The parent node for nodes with primary type nt:resource or dna:resource must be of type nt:file. missingRequiredProperty = Missing required property "{3}" at path "{0}" in workspace "{1}" in {2} - +nodeOrderingNotSupported = {0} does not support node ordering # Writable tests couldNotCreateFile =Error reading data at path "{0}" in workspace "{1}" in source "{2}": "{3}" Index: extensions/modeshape-connector-svn/src/test/java/org/modeshape/connector/svn/SvnRepositoryConnectorWritableTest.java =================================================================== --- extensions/modeshape-connector-svn/src/test/java/org/modeshape/connector/svn/SvnRepositoryConnectorWritableTest.java (revision 1825) +++ extensions/modeshape-connector-svn/src/test/java/org/modeshape/connector/svn/SvnRepositoryConnectorWritableTest.java (working copy) @@ -26,6 +26,7 @@ package org.modeshape.connector.svn; import static org.hamcrest.core.Is.is; import static org.hamcrest.core.IsNull.notNullValue; import static org.junit.Assert.assertThat; +import static org.junit.Assert.fail; import java.io.ByteArrayOutputStream; import org.junit.Test; import org.modeshape.graph.Graph; @@ -33,6 +34,7 @@ import org.modeshape.graph.JcrLexicon; import org.modeshape.graph.JcrMixLexicon; import org.modeshape.graph.JcrNtLexicon; import org.modeshape.graph.ModeShapeLexicon; +import org.modeshape.graph.Graph.Batch; import org.modeshape.graph.connector.RepositorySource; import org.modeshape.graph.connector.RepositorySourceException; import org.modeshape.graph.connector.test.AbstractConnectorTest; @@ -283,6 +285,65 @@ public class SvnRepositoryConnectorWritableTest extends AbstractConnectorTest { } @Test + public void shouldBeAbleToMoveFile() throws Exception { + graph.create("/testFile").with(JcrLexicon.PRIMARY_TYPE, JcrNtLexicon.FILE).orReplace().and(); + graph.create("/testFile/jcr:content").with(JcrLexicon.PRIMARY_TYPE, ModeShapeLexicon.RESOURCE).and(JcrLexicon.DATA, + TEST_CONTENT.getBytes()).orReplace().and(); + + kind = remoteRepos.checkPath("testFile", -1); + assertThat(kind == SVNNodeKind.FILE, is(Boolean.TRUE)); + fileProperties = new SVNProperties(); + baos = new ByteArrayOutputStream(); + remoteRepos.getFile("testFile", -1, fileProperties, baos); + assertContents(baos, TEST_CONTENT); + + graph.move("/testFile").as("newFile").into("/").and(); + kind = remoteRepos.checkPath("newFile", -1); + assertThat(kind == SVNNodeKind.FILE, is(Boolean.TRUE)); + fileProperties = new SVNProperties(); + baos = new ByteArrayOutputStream(); + remoteRepos.getFile("newFile", -1, fileProperties, baos); + assertContents(baos, TEST_CONTENT); + + try { + graph.getNodeAt("/testFile"); + fail("Old copy of file still exists at source location"); + } catch (PathNotFoundException expected) { + + } + } + + @Test + public void shouldBeAbleToMoveFolder() throws Exception { + graph.create("/testFolder").orReplace().and(); + graph.create("/testFolder/testFile").with(JcrLexicon.PRIMARY_TYPE, JcrNtLexicon.FILE).orReplace().and(); + graph.create("/testFolder/testFile/jcr:content").with(JcrLexicon.PRIMARY_TYPE, ModeShapeLexicon.RESOURCE).and(JcrLexicon.DATA, + TEST_CONTENT.getBytes()).orReplace().and(); + + kind = remoteRepos.checkPath("testFolder/testFile", -1); + assertThat(kind == SVNNodeKind.FILE, is(Boolean.TRUE)); + fileProperties = new SVNProperties(); + baos = new ByteArrayOutputStream(); + remoteRepos.getFile("testFolder/testFile", -1, fileProperties, baos); + assertContents(baos, TEST_CONTENT); + + graph.move("/testFolder").as("newFolder").into("/").and(); + kind = remoteRepos.checkPath("newFolder", -1); + assertThat(kind == SVNNodeKind.DIR, is(Boolean.TRUE)); + fileProperties = new SVNProperties(); + baos = new ByteArrayOutputStream(); + remoteRepos.getFile("newFolder/testFile", -1, fileProperties, baos); + assertContents(baos, TEST_CONTENT); + try { + graph.getNodeAt("/testFolder"); + fail("Old copy of file still exists at source location"); + } catch (PathNotFoundException expected) { + + } + + } + + @Test public void shouldBeAbleToDeleteFolder() throws Exception { graph.create("/testFolder").orReplace().and(); graph.create("/testFolder/testFile").with(JcrLexicon.PRIMARY_TYPE, JcrNtLexicon.FILE).orReplace().and(); @@ -352,6 +413,39 @@ public class SvnRepositoryConnectorWritableTest extends AbstractConnectorTest { assertContents(baos, ""); } + @Test + public void shouldBeAllOrNothing() { + String badTestFileName = "/missingDirectory/someFile"; + + Batch batch = graph.batch(); + + batch.create("/testFile").with(JcrLexicon.PRIMARY_TYPE, JcrNtLexicon.FILE).orReplace().and(); + batch.create("/testFile/jcr:content").with(JcrLexicon.PRIMARY_TYPE, ModeShapeLexicon.RESOURCE).and(JcrLexicon.DATA, + TEST_CONTENT.getBytes()).orReplace().and(); + batch.create(badTestFileName).and(); + + try { + batch.execute(); + fail("The invalid test file name (" + badTestFileName + ") did not fail"); + } catch (PathNotFoundException rse) { + // Expected + } + + try { + graph.getNodeAt("/testFile"); + fail("Got node at /testFile -- whole transaction should have failed"); + } catch (PathNotFoundException expected) { + // Expected + } + } + + @Test + public void runItTwice() { + shouldBeAllOrNothing(); + shouldBeAllOrNothing(); + shouldBeAllOrNothing(); + } + protected void assertContents( ByteArrayOutputStream baos, String contents ) { assertThat(baos, notNullValue()); Index: extensions/modeshape-connector-svn/src/test/java/org/modeshape/connector/svn/SvnRepositorySourceTest.java =================================================================== --- extensions/modeshape-connector-svn/src/test/java/org/modeshape/connector/svn/SvnRepositorySourceTest.java (revision 1825) +++ extensions/modeshape-connector-svn/src/test/java/org/modeshape/connector/svn/SvnRepositorySourceTest.java (working copy) @@ -252,7 +252,8 @@ public class SvnRepositorySourceTest { assertThat((String)refAttributes.remove(SvnRepositorySource.SVN_REPOSITORY_ROOT_URL), is(source.getRepositoryRootUrl())); assertThat((String)refAttributes.remove(SvnRepositorySource.SVN_USERNAME), is(source.getUsername())); assertThat((String)refAttributes.remove(SvnRepositorySource.SVN_PASSWORD), is(source.getPassword())); - assertThat((String)refAttributes.remove(SvnRepositorySource.ROOT_NODE_UUID), is(source.getRootNodeUuid().toString())); + assertThat((String)refAttributes.remove(SvnRepositorySource.ROOT_NODE_UUID), + is(source.getRootNodeUuidObject().toString())); assertThat((String)refAttributes.remove(SvnRepositorySource.RETRY_LIMIT), is(Integer.toString(source.getRetryLimit()))); assertThat((String)refAttributes.remove(SvnRepositorySource.ALLOW_CREATING_WORKSPACES), is(Boolean.toString(source.isCreatingWorkspacesAllowed()))); Index: modeshape-graph/src/main/java/org/modeshape/graph/connector/base/Connection.java =================================================================== --- modeshape-graph/src/main/java/org/modeshape/graph/connector/base/Connection.java (revision 1825) +++ modeshape-graph/src/main/java/org/modeshape/graph/connector/base/Connection.java (working copy) @@ -119,6 +119,7 @@ public class Connection } } catch (Throwable error) { commit = false; + error.printStackTrace(); } finally { try { processor.close(); @@ -132,6 +133,7 @@ public class Connection txn.rollback(); } } catch (Throwable commitOrRollbackError) { + commitOrRollbackError.printStackTrace(); if (commit && !request.hasError() && !request.isFrozen()) { // Record the error on the request ... request.setError(commitOrRollbackError); Index: modeshape-graph/src/main/java/org/modeshape/graph/connector/base/PathTransaction.java =================================================================== --- modeshape-graph/src/main/java/org/modeshape/graph/connector/base/PathTransaction.java (revision 1825) +++ modeshape-graph/src/main/java/org/modeshape/graph/connector/base/PathTransaction.java (working copy) @@ -239,13 +239,13 @@ public abstract class PathTransaction properties ) { - assert uuid == null : "UUID should always be null for a PathTransaction"; WorkspaceChanges changes = getChangesFor(workspace, true); // If the parent doesn't already have changes, we need to find the new parent in the newWorkspace's changes if (!parent.hasChanges()) { parent = getNode(workspace, locationFor(parent)); } + NodeType oldParent = (NodeType)parent.clone(); NodeType newNode = null; if (index < 0) { @@ -272,8 +272,9 @@ public abstract class PathTransaction iter = siblings.listIterator(oldIndex); iter.hasNext();) { NodeType sibling = iter.next(); if (sibling.getName().getName().equals(newChildName)) { + NodeType oldSibling = (NodeType)sibling.clone(); sibling = (NodeType)sibling.withName(pathFactory.createSegment(newChildName, snsIndex++)); - changes.changed(sibling); + changes.changed(oldSibling, sibling); } } } @@ -377,8 +380,9 @@ public abstract class PathTransaction siblings = getChildren(workspace, parent); @@ -524,8 +530,9 @@ public abstract class PathTransaction iter = siblings.listIterator(index); iter.hasNext();) { NodeType sibling = iter.next(); if (sibling.getName().getName().equals(name)) { + NodeType oldSibling = (NodeType)sibling.clone(); sibling = (NodeType)sibling.withName(pathFactory.createSegment(name, snsIndex++)); - changes.changed(sibling); + changes.changed(oldSibling, sibling); } } } @@ -544,10 +551,11 @@ public abstract class PathTransaction propertiesToSet, Iterable propertiesToRemove, boolean removeAllExisting ) { + NodeType oldCopy = (NodeType)node.clone(); NodeType copy = (NodeType)node.withProperties(propertiesToSet, propertiesToRemove, removeAllExisting); if (copy != node) { WorkspaceChanges changes = getChangesFor(workspace, true); - changes.changed(copy); + changes.changed(oldCopy, copy); } return copy; } @@ -599,10 +608,6 @@ public abstract class PathTransaction implements Worksp * @throws UnsupportedOperationException by default, subclasses should override this method so that this exception is not * thrown * @see #createMoveCommand(PathNode, PathNode) - * @see #createPutCommand(PathNode) + * @see #createPutCommand(PathNode, PathNode) * @see #createRemoveCommand(Path) */ public NodeType putNode( NodeType node ) { @@ -136,7 +136,7 @@ public abstract class PathWorkspace implements Worksp * @throws UnsupportedOperationException by default, subclasses should override this method so that this exception is not * thrown * @see #createMoveCommand(PathNode, PathNode) - * @see #createPutCommand(PathNode) + * @see #createPutCommand(PathNode, PathNode) * @see #createRemoveCommand(Path) */ public NodeType moveNode( NodeType source, @@ -153,7 +153,7 @@ public abstract class PathWorkspace implements Worksp * @throws UnsupportedOperationException by default, subclasses should override this method so that this exception is not * thrown * @see #createMoveCommand(PathNode, PathNode) - * @see #createPutCommand(PathNode) + * @see #createPutCommand(PathNode, PathNode) * @see #createRemoveCommand(Path) */ public NodeType removeNode( Path path ) { @@ -196,12 +196,13 @@ public abstract class PathWorkspace implements Worksp /** * Create a change command for the required update to the given node * + * @param oldNode the prior version of the node; may be null if this is a new node * @param node the new version of the node; may not be null * @return a {@link ChangeCommand} instance that reflects the changes to the node - * @see #createPutCommand(PathNode) * @see #commit(List) */ - public ChangeCommand createPutCommand( NodeType node ) { + public ChangeCommand createPutCommand( NodeType oldNode, + NodeType node ) { return new PutCommand(node); } @@ -210,7 +211,7 @@ public abstract class PathWorkspace implements Worksp * * @param path the path to the node at the root of the branch to be removed; may not be null * @return a {@link ChangeCommand} instance that reflects the changes to the node - * @see #createPutCommand(PathNode) + * @see #createPutCommand(PathNode, PathNode) * @see #commit(List) */ public ChangeCommand createRemoveCommand( Path path ) { @@ -220,7 +221,7 @@ public abstract class PathWorkspace implements Worksp /** * Create a change command that represents the movement of a node. The movement record will only reflect the changes to the * node's name and/or parent. Changes to the node's properties or children should be ignored. A separate - * {@link #createPutCommand(PathNode) put command} should be used to reflect these changes. + * {@link #createPutCommand(PathNode, PathNode) put command} should be used to reflect these changes. * * @param source the original version of the node; may not be null * @param target the new version of the node; may not be null