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 1635) +++ extensions/modeshape-connector-svn/src/main/java/org/modeshape/connector/svn/SvnRepository.java (working copy) @@ -18,11 +18,11 @@ 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.ModeShapeIntLexicon; -import org.modeshape.graph.ModeShapeLexicon; 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; @@ -248,8 +248,7 @@ public class SvnRepository extends WritablePathRepository { try { if (SvnRepositoryUtil.exists(workspaceRoot, newChildPath)) { if (conflictBehavior.equals(NodeConflictBehavior.APPEND)) { - I18n msg = SvnRepositoryConnectorI18n.sameNameSiblingsAreNotAllowed; - throw new InvalidRequestException(msg.text("SVN Connector does not support Same Name Sibling")); + throw new InvalidRequestException(SvnRepositoryConnectorI18n.sameNameSiblingsAreNotAllowed.text()); } else if (conflictBehavior.equals(NodeConflictBehavior.DO_NOT_REPLACE)) { skipWrite = true; } @@ -308,26 +307,27 @@ public class SvnRepository extends WritablePathRepository { throw new RepositorySourceException(getSourceName(), msg.text(parentPathAsString, getName(), getSourceName())); } - boolean skipWrite = false; - if (conflictBehavior.equals(NodeConflictBehavior.APPEND)) { - I18n msg = SvnRepositoryConnectorI18n.sameNameSiblingsAreNotAllowed; - throw new InvalidRequestException(msg.text("SVN Connector does not support Same Name Sibling")); - } else if (conflictBehavior.equals(NodeConflictBehavior.DO_NOT_REPLACE)) { - // TODO check if the file already has content - skipWrite = true; + 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; } - if (!skipWrite) { - Property dataProperty = properties.get(JcrLexicon.DATA); - if (dataProperty == null) { - I18n msg = SvnRepositoryConnectorI18n.missingRequiredProperty; - String dataPropName = JcrLexicon.DATA.getString(registry); - throw new RepositorySourceException(getSourceName(), msg.text(parentPathAsString, - getName(), - getSourceName(), - dataPropName)); - } - + 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 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 1635) +++ extensions/modeshape-connector-svn/src/main/java/org/modeshape/connector/svn/SvnRepositoryConnectorI18n.java (working copy) @@ -85,5 +85,4 @@ public final class SvnRepositoryConnectorI18n { return I18n.getLocalizationProblems(SvnRepositoryConnectorI18n.class, locale); } - } 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 1635) +++ extensions/modeshape-connector-svn/src/main/resources/org/modeshape/connector/svn/SvnRepositoryConnectorI18n.properties (working copy) @@ -40,7 +40,7 @@ pathForDefaultWorkspaceCannotBeRead = The path "{0}" for the default workspace f pathForPredefinedWorkspaceDoesNotExist = The path "{0}" for the predefined workspace for the file system source "{1}" does not represent an existing directory pathForPredefinedWorkspaceIsNotDirectory = The path "{0}" for the predefined workspace for the file system source "{1}" is actually a path to an existing file pathForPredefinedWorkspaceCannotBeRead = The path "{0}" for the predefined workspace for the file system source "{1}" cannot be read -sameNameSiblingsAreNotAllowed = {0} +sameNameSiblingsAreNotAllowed = The SVN Connector does not support sibling nodes with the same name (e.g., same name siblings) onlyTheDefaultNamespaceIsAllowed = {0} requires node names use the default namespace: {1} locationInRequestMustHavePath = {0} requires a path in the request: {1} unableToCreateWorkspaces = {0} does not allow creating new workspaces (request was to create "{1}") Index: extensions/modeshape-connector-svn/src/test/java/org/modeshape/connector/svn/SvnIntegrationTest.java =================================================================== --- extensions/modeshape-connector-svn/src/test/java/org/modeshape/connector/svn/SvnIntegrationTest.java (revision 1635) +++ extensions/modeshape-connector-svn/src/test/java/org/modeshape/connector/svn/SvnIntegrationTest.java (working copy) @@ -27,6 +27,8 @@ import static org.hamcrest.core.Is.is; import static org.hamcrest.core.IsNull.notNullValue; import static org.junit.Assert.assertThat; import java.util.Map; +import org.junit.Before; +import org.junit.Test; import org.modeshape.graph.ExecutionContext; import org.modeshape.graph.Graph; import org.modeshape.graph.Location; @@ -39,8 +41,6 @@ import org.modeshape.graph.connector.RepositorySourceException; import org.modeshape.graph.observe.Observer; import org.modeshape.graph.property.Name; import org.modeshape.graph.property.Property; -import org.junit.Before; -import org.junit.Test; public class SvnIntegrationTest { @@ -51,7 +51,7 @@ public class SvnIntegrationTest { @Before public void beforeEach() { - repositoryUrl = "http://anonsvn.jboss.org/repos/dna/"; + repositoryUrl = "http://anonsvn.jboss.org/repos/modeshape/"; predefinedWorkspaceNames = new String[] {"trunk", "tags", "branches"}; context = new ExecutionContext(); source = new SvnRepositorySource(); 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 1635) +++ modeshape-graph/src/main/java/org/modeshape/graph/request/processor/RequestProcessor.java (working copy) @@ -308,7 +308,9 @@ public abstract class RequestProcessor { boolean hasErrors = false; boolean readonly = request.isReadOnly(); // Iterate over the requests in this composite, but only iterate once so that - for (Request embedded : request) { + Iterator iter = request.iterator(); + while (iter.hasNext()) { + Request embedded = iter.next(); assert embedded != null; if (embedded.isCancelled()) return; process(embedded); @@ -320,6 +322,13 @@ public abstract class RequestProcessor { // on the composite request ... assert embedded.getError() != null; request.setError(embedded.getError()); + // We need to freeze all the remaining (unprocessed) requests before returning ... + while (iter.hasNext()) { + embedded = iter.next(); + // Cancel this request and then freeze ... + embedded.cancel(); + embedded.freeze(); + } return; } } Index: modeshape-integration-tests/src/test/java/org/modeshape/test/integration/svn/JcrAndLocalSvnRepositoryTest.java new file mode 100644 =================================================================== --- /dev/null (revision 1635) +++ modeshape-integration-tests/src/test/java/org/modeshape/test/integration/svn/JcrAndLocalSvnRepositoryTest.java (working copy) @@ -0,0 +1,227 @@ +/* + * JBoss DNA (http://www.jboss.org/dna) + * 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. + * + * JBoss DNA is free software. Unless otherwise indicated, all code in JBoss DNA + * 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. + * + * JBoss DNA 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.test.integration.svn; + +import static org.hamcrest.core.Is.is; +import static org.hamcrest.core.IsNull.notNullValue; +import static org.junit.Assert.assertThat; +import java.io.File; +import java.io.IOException; +import java.util.Calendar; +import javax.jcr.Node; +import javax.jcr.NodeIterator; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.modeshape.common.util.FileUtil; +import org.modeshape.common.util.IoUtil; +import org.modeshape.connector.svn.SvnRepositorySource; +import org.modeshape.graph.SecurityContext; +import org.modeshape.jcr.JcrConfiguration; +import org.modeshape.jcr.JcrEngine; +import org.modeshape.jcr.SecurityContextCredentials; +import org.tmatesoft.svn.core.SVNException; +import org.tmatesoft.svn.core.SVNURL; + +public class JcrAndLocalSvnRepositoryTest { + private boolean print; + private JcrEngine engine; + private Session session; + + @Before + public void beforeEach() throws Exception { + print = false; + + // Copy our repository into the target area ... + File source = new File("src/test/resources/svn/local_repos/dummy_svn_repos"); + File target = new File("target/local_svn_repos"); + FileUtil.delete(target); + int num = FileUtil.copy(source, target); + print("Copied " + num + " files and directories"); + + // Create the engine that uses the local SVN repository ... + final String repositoryUrl = svnUrlFor(target); + final String[] predefinedWorkspaceNames = new String[] {"trunk", "tags"}; + final String svnRepositorySource = "svnRepositorySource"; + final String repositoryName = "svnRepository"; + final JcrConfiguration configuration = new JcrConfiguration(); + configuration.repositorySource(svnRepositorySource) + .usingClass(SvnRepositorySource.class) + .setProperty("password", "") + .setProperty("username", "anonymous") + .setProperty("repositoryRootUrl", repositoryUrl) + .setProperty("predefinedWorkspaceNames", predefinedWorkspaceNames) + .setProperty("defaultWorkspaceName", predefinedWorkspaceNames[0]) + .setProperty("creatingWorkspacesAllowed", false) + .setProperty("updatesAllowed", true) + .and() + .repository(repositoryName) + .setDescription("The JCR repository backed by a local SVN") + .setSource(svnRepositorySource); + this.engine = configuration.save().and().build(); + this.engine.start(); + + print("Using local SVN repository " + repositoryUrl); + + this.session = this.engine.getRepository(repositoryName) + .login(new SecurityContextCredentials(new MyCustomSecurityContext())); + + } + + @After + public void afterEach() throws Exception { + if (this.session != null) { + try { + this.session.logout(); + } finally { + this.session = null; + if (this.engine != null) { + this.engine.shutdown(); + } + } + } + } + + @Test + public void shouldIterateOverChildrenOfRoot() throws Exception { + print("Getting the root node and it's children (to a maximum depth of 4)..."); + Node root = session.getRootNode(); + printSubgraph(root, 4, " "); + + Node h = root.getNode("root/c/h"); + assertThatNodeIsFolder(h); + Node dnaSubmission = root.getNode("root/c/h/JBoss DNA Submission Receipt for JBoss World 2009.pdf"); + assertThatNodeIsFile(dnaSubmission, "application/octet-stream", null); + } + + @Test + public void shouldAllowingAddingFile() throws Exception { + Node rootNode = session.getRootNode(); + + File file = new File("src/test/resources/log4j.properties"); + String fileContent = IoUtil.read(file); + assertThat(file.exists() && file.isFile(), is(true)); + assertThat(fileContent, is(notNullValue())); + + Node fileNode = rootNode.addNode(file.getName(), "nt:file"); + Node contentNode = fileNode.addNode("jcr:content", "nt:resource"); + contentNode.setProperty("jcr:mimeType", "text/plain"); + contentNode.setProperty("jcr:lastModified", Calendar.getInstance()); + contentNode.setProperty("jcr:data", fileContent); + + System.out.println("## STARTING TO SAVE ##"); + rootNode.getSession().save(); + System.out.println("## SAVED ##"); + } + + protected void print( String str ) { + if (print) System.out.println(str); + } + + protected void printSubgraph( Node node, + int depth, + String prefix ) throws RepositoryException { + NodeIterator nodeIterator = node.getNodes(); + while (nodeIterator.hasNext()) { + Node child = nodeIterator.nextNode(); + print((prefix != null ? prefix : " ") + child); + if (depth > 1) printSubgraph(child, depth - 1, prefix); + } + } + + public void assertThatNodeIsFolder( Node node ) throws RepositoryException { + assertThat(node, is(notNullValue())); + assertThat(node.getProperty("jcr:primaryType").getString(), is("nt:folder")); + } + + public void assertThatNodeIsFile( Node node, + String mimeType, + String contents ) throws RepositoryException { + assertThat(node, is(notNullValue())); + assertThat(node.getProperty("jcr:primaryType").getString(), is("nt:file")); + + // Check that there is one child, and that the child is "jcr:content" ... + NodeIterator nodeIterator = node.getNodes(); + assertThat(nodeIterator.getSize() >= 1L, is(true)); + + // Check that the "jcr:content" node is correct ... + Node jcrContent = node.getNode("jcr:content"); + assertThat(jcrContent.getProperty("jcr:mimeType").getString(), is(mimeType)); + if (contents != null) { + assertThat(jcrContent.getProperty("jcr:data").getString(), is(contents)); + } + } + + protected class MyCustomSecurityContext implements SecurityContext { + /** + * {@inheritDoc} + * + * @see org.modeshape.graph.SecurityContext#getUserName() + */ + public String getUserName() { + return "Fred"; + } + + /** + * {@inheritDoc} + * + * @see org.modeshape.graph.SecurityContext#hasRole(java.lang.String) + */ + public boolean hasRole( String roleName ) { + return true; + } + + /** + * {@inheritDoc} + * + * @see org.modeshape.graph.SecurityContext#logout() + */ + public void logout() { + // do something + } + } + + /** + * Create an OS-independent URL for a local SVN repository. + * + * @param directory the directory representing the root of the local SVN repository + * @return the URL as a string + * @throws IOException if there is a problem obtaining the canonical URI for the supplied directory + * @throws SVNException if there is a problem parsing or decoding the URL + */ + protected static String svnUrlFor( File directory ) throws IOException, SVNException { + String url = directory.getCanonicalFile().toURI().toURL().toExternalForm(); + + url = url.replaceFirst("file:/", "file://localhost/"); + + // Have to decode the URL ... + SVNURL encodedUrl = SVNURL.parseURIEncoded(url); + url = encodedUrl.toDecodedString(); + + if (!url.endsWith("/")) url = url + "/"; + return url; + } +} Index: modeshape-jcr/src/main/java/org/modeshape/jcr/AbstractJcrProperty.java =================================================================== --- modeshape-jcr/src/main/java/org/modeshape/jcr/AbstractJcrProperty.java (revision 1635) +++ modeshape-jcr/src/main/java/org/modeshape/jcr/AbstractJcrProperty.java (working copy) @@ -23,6 +23,7 @@ */ package org.modeshape.jcr; +import java.util.Iterator; import javax.jcr.InvalidItemStateException; import javax.jcr.Item; import javax.jcr.ItemNotFoundException; @@ -40,6 +41,7 @@ import net.jcip.annotations.NotThreadSafe; import org.modeshape.common.util.CheckArg; import org.modeshape.graph.property.Name; import org.modeshape.graph.property.Path; +import org.modeshape.graph.property.ValueFactory; import org.modeshape.graph.session.GraphSession.PropertyInfo; import org.modeshape.jcr.SessionCache.JcrPropertyPayload; import org.modeshape.jcr.SessionCache.NodeEditor; @@ -275,4 +277,33 @@ abstract class AbstractJcrProperty extends AbstractJcrItem implements Property, throw new RuntimeException(e); } } + + /** + * {@inheritDoc} + * + * @see java.lang.Object#toString() + */ + @Override + public String toString() { + try { + ValueFactory stringFactory = session().getExecutionContext().getValueFactories().getStringFactory(); + StringBuilder sb = new StringBuilder(); + sb.append(getName()).append('='); + org.modeshape.graph.property.Property property = propertyInfo().getProperty(); + if (isMultiple()) { + sb.append('['); + Iterator iter = property.iterator(); + if (iter.hasNext()) { + sb.append(stringFactory.create(iter.next())); + if (iter.hasNext()) sb.append(','); + } + sb.append(']'); + } else { + sb.append(stringFactory.create(property.getFirstValue())); + } + return sb.toString(); + } catch (RepositoryException e) { + return super.toString(); + } + } } Index: modeshape-jcr/src/main/java/org/modeshape/jcr/JcrRepository.java =================================================================== --- modeshape-jcr/src/main/java/org/modeshape/jcr/JcrRepository.java (revision 1635) +++ modeshape-jcr/src/main/java/org/modeshape/jcr/JcrRepository.java (working copy) @@ -926,7 +926,13 @@ public class JcrRepository implements Repository { // We're not sharing a '/jcr:system' branch, so we need to make sure there is one in the source. // Note that this doesn't always work with some connectors (e.g., the FileSystem or SVN connectors) // that don't allow arbitrary nodes. - initializeSystemContent(graph); + try { + initializeSystemContent(graph); + } catch (RepositorySourceException e) { + Logger.getLogger(getClass()) + .debug(e, + "Workspaces do not share a common /jcr:system branch, but the connector was unable to create one in this session. Errors may result."); + } } // Create the workspace, which will create its own session ...