[dna-commits] DNA SVN: r868 - in trunk: dna-graph/src/main/java/org/jboss/dna/graph/io and 6 other directories.

dna-commits at lists.jboss.org dna-commits at lists.jboss.org
Thu Apr 30 12:16:00 EDT 2009


Author: rhauch
Date: 2009-04-30 12:15:59 -0400 (Thu, 30 Apr 2009)
New Revision: 868

Added:
   trunk/dna-graph/src/test/resources/xmlHandler/docWithNestedProperties.xml
Modified:
   trunk/dna-graph/pom.xml
   trunk/dna-graph/src/main/java/org/jboss/dna/graph/io/Destination.java
   trunk/dna-graph/src/main/java/org/jboss/dna/graph/io/GraphBatchDestination.java
   trunk/dna-graph/src/main/java/org/jboss/dna/graph/request/BatchRequestBuilder.java
   trunk/dna-graph/src/main/java/org/jboss/dna/graph/xml/XmlHandler.java
   trunk/dna-graph/src/test/java/org/jboss/dna/graph/io/GraphImporterTest.java
   trunk/dna-graph/src/test/java/org/jboss/dna/graph/xml/XmlHandlerTest.java
   trunk/extensions/dna-sequencer-cnd/src/main/java/org/jboss/dna/sequencer/cnd/CndSequencer.java
Log:
DNA-378 XML Graph Importer should support importing multi-valued properties

Applied the patch, which adds support for multi-valued properties as child elements (intermixed with child elements for nodes).

Modified: trunk/dna-graph/pom.xml
===================================================================
--- trunk/dna-graph/pom.xml	2009-04-30 15:31:52 UTC (rev 867)
+++ trunk/dna-graph/pom.xml	2009-04-30 16:15:59 UTC (rev 868)
@@ -32,6 +32,11 @@
       <groupId>joda-time</groupId>
       <artifactId>joda-time</artifactId>
     </dependency>
+    <dependency>
+        <groupId>com.google.code.google-collections</groupId>
+        <artifactId>google-collect</artifactId>
+        <version>snapshot-20080530</version>
+    </dependency>
     <!-- 
     Testing (note the scope)
     -->

Modified: trunk/dna-graph/src/main/java/org/jboss/dna/graph/io/Destination.java
===================================================================
--- trunk/dna-graph/src/main/java/org/jboss/dna/graph/io/Destination.java	2009-04-30 15:31:52 UTC (rev 867)
+++ trunk/dna-graph/src/main/java/org/jboss/dna/graph/io/Destination.java	2009-04-30 16:15:59 UTC (rev 868)
@@ -66,6 +66,15 @@
                         Property... additionalProperties );
 
     /**
+     * Sets the given properties on the node at the supplied path. The path will be absolute.
+     * 
+     * @param path the absolute path of the node
+     * @param properties the remaining properties for the node
+     */
+    public void setProperties( Path path,
+                               Property... properties );
+
+    /**
      * Signal to this destination that any enqueued create requests should be submitted. Usually this happens at the end of the
      * document parsing, but an implementer must allow for it to be called multiple times and anytime during parsing.
      */

Modified: trunk/dna-graph/src/main/java/org/jboss/dna/graph/io/GraphBatchDestination.java
===================================================================
--- trunk/dna-graph/src/main/java/org/jboss/dna/graph/io/GraphBatchDestination.java	2009-04-30 15:31:52 UTC (rev 867)
+++ trunk/dna-graph/src/main/java/org/jboss/dna/graph/io/GraphBatchDestination.java	2009-04-30 16:15:59 UTC (rev 868)
@@ -117,6 +117,18 @@
 
     /**
      * {@inheritDoc}
+     *
+     * @see org.jboss.dna.graph.io.Destination#setProperties(org.jboss.dna.graph.property.Path, org.jboss.dna.graph.property.Property[])
+     */
+    public void setProperties( Path path, 
+                               Property... properties ) {
+        if (properties == null) return;
+        
+        batch.set(properties).on(path);
+    }
+
+    /**
+     * {@inheritDoc}
      * 
      * @see org.jboss.dna.graph.io.Destination#submit()
      */

Modified: trunk/dna-graph/src/main/java/org/jboss/dna/graph/request/BatchRequestBuilder.java
===================================================================
--- trunk/dna-graph/src/main/java/org/jboss/dna/graph/request/BatchRequestBuilder.java	2009-04-30 15:31:52 UTC (rev 867)
+++ trunk/dna-graph/src/main/java/org/jboss/dna/graph/request/BatchRequestBuilder.java	2009-04-30 16:15:59 UTC (rev 868)
@@ -94,7 +94,7 @@
                 return newRequest;
             }
             // We have at least one other request, so add the pending request ...
-            add(newRequest);
+            addPending();
             ++number;
         } else {
             // There is no pending request ...

Modified: trunk/dna-graph/src/main/java/org/jboss/dna/graph/xml/XmlHandler.java
===================================================================
--- trunk/dna-graph/src/main/java/org/jboss/dna/graph/xml/XmlHandler.java	2009-04-30 15:31:52 UTC (rev 867)
+++ trunk/dna-graph/src/main/java/org/jboss/dna/graph/xml/XmlHandler.java	2009-04-30 16:15:59 UTC (rev 868)
@@ -24,6 +24,9 @@
 package org.jboss.dna.graph.xml;
 
 import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.LinkedList;
 import java.util.List;
@@ -45,6 +48,8 @@
 import org.jboss.dna.graph.property.basic.LocalNamespaceRegistry;
 import org.xml.sax.Attributes;
 import org.xml.sax.ext.DefaultHandler2;
+import com.google.common.collect.LinkedHashMultimap;
+import com.google.common.collect.Multimap;
 
 /**
  * A {@link DefaultHandler2} specialization that responds to XML content events by creating the corresponding content in the
@@ -162,6 +167,20 @@
     protected final Object[] propertyValues = new Object[1];
 
     /**
+     * Character buffer to aggregate nested character data
+     * 
+     * @see ElementEntry
+     */
+    private StringBuilder characterDataBuffer = new StringBuilder();
+
+    /**
+     * Stack of pending {@link ElementEntry element entries} from the root of the imported content to the current node.
+     * 
+     * @see ElementEntry
+     */
+    private final LinkedList<ElementEntry> elementStack = new LinkedList<ElementEntry>();
+
+    /**
      * Create a handler that creates content in the supplied graph
      * 
      * @param destination the destination where the content should be sent.graph in which the content should be placed
@@ -310,6 +329,16 @@
         assert localName != null;
         Name nodeName = null;
 
+        ElementEntry element;
+        if (!elementStack.isEmpty()) {
+            // Add the parent
+            elementStack.peek().addAsNode();
+            element = new ElementEntry(elementStack.peek(), currentPath, null);
+        } else {
+            element = new ElementEntry(null, currentPath, null);
+        }
+        elementStack.addFirst(element);
+
         properties.clear();
         Object typePropertyValue = null;
         // Convert each of the attributes to a property ...
@@ -333,6 +362,7 @@
             // Check to see if this is an attribute that represents the node name (which may be null) ...
             if (nodeName == null && attributeName.equals(nameAttribute)) {
                 nodeName = nameFactory.create(attributes.getValue(i)); // don't use a decoder
+                element.setName(nodeName);
                 continue;
             }
             if (typePropertyValue == null && attributeName.equals(typeAttribute)) {
@@ -340,13 +370,13 @@
                 continue;
             }
             // Create a property for this attribute ...
-            Property property = createProperty(attributeName, attributes.getValue(i));
-            properties.add(property);
+            element.addProperty(attributeName, attributes.getValue(i));
         }
         // Create the node name if required ...
         if (nodeName == null) {
             // No attribute defines the node name ...
             nodeName = nameFactory.create(uri, localName, decoder);
+            element.setName(nodeName);
         } else {
             if (typePropertyValue == null) typePropertyValue = nameFactory.create(uri, localName, decoder);
         }
@@ -354,15 +384,12 @@
             // A attribute defines the node name. Set the type property, if required
             if (typePropertyValue == null) typePropertyValue = typeAttributeValue;
             if (typePropertyValue != null) {
-                propertyValues[0] = typePropertyValue;
-                Property property = propertyFactory.create(typeAttribute, propertyValues);
-                properties.add(property);
+                element.addProperty(typeAttribute, typePropertyValue);
             }
         }
+
         // Update the current path ...
-        currentPath = pathFactory.create(currentPath, nodeName);
-        // Create the node, and note that we don't care about same-name siblings (as the graph will correct them) ...
-        destination.create(currentPath, properties);
+        currentPath = element.path();
     }
 
     /**
@@ -374,6 +401,15 @@
     public void endElement( String uri,
                             String localName,
                             String name ) {
+
+        String s = characterDataBuffer.toString().trim();
+        if (s.length() > 0) {
+            elementStack.removeFirst().addAsPropertySetTo(s);
+        } else if (!elementStack.isEmpty()) {
+            elementStack.removeFirst().submit();
+        }
+        characterDataBuffer = new StringBuilder();
+
         // Nothing to do but to change the current path to be the parent ...
         currentPath = currentPath.getParent();
     }
@@ -381,6 +417,19 @@
     /**
      * {@inheritDoc}
      * 
+     * @see org.xml.sax.helpers.DefaultHandler#characters(char[], int, int)
+     */
+    @Override
+    public void characters( char[] ch,
+                            int start,
+                            int length ) {
+        // Have to add this to a buffer as one logical set of character data can cause this method to fire multiple times
+        characterDataBuffer.append(ch, start, length);
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
      * @see org.xml.sax.helpers.DefaultHandler#endDocument()
      */
     @Override
@@ -402,8 +451,118 @@
     protected Property createProperty( Name propertyName,
                                        Object value ) {
         propertyValues[0] = value;
-        Property result = propertyFactory.create(propertyName, propertyValues);
-        return result;
+        return propertyFactory.create(propertyName, propertyValues);
     }
 
+    /**
+     * Create a property with the given name and values, obtained from an attribute name and value in the XML content.
+     * <p>
+     * By default, this method creates a property by directly using the values as the values of the property.
+     * </p>
+     * 
+     * @param propertyName the name of the property; never null
+     * @param values the attribute values
+     * @return the property; may not be null
+     */
+    protected Property createProperty( Name propertyName,
+                                       Collection<Object> values ) {
+        return propertyFactory.create(propertyName, values);
+    }
+
+    /**
+     * Possible states for an {@link ElementEntry} instance. All element entries start in state {@code TBD} and then transition to
+     * one of the terminating states, {@code NODE} or {@code PROPERTY} when {@link ElementEntry#addAsNode()} or
+     * {@link ElementEntry#addAsPropertySetTo(Object)} is invoked.
+     */
+    protected enum ElementEntryState {
+        NODE,
+        PROPERTY,
+        TBD
+    }
+
+    /**
+     * Element entries hold references to the data of "pending" elements. "Pending" elements are elements which have been
+     * encountered through a {@link XmlHandler#startElement(String, String, String, Attributes)} event but have not yet been fully
+     * committed to the {@link XmlHandler#destination}.
+     * <p>
+     * As the current import semantics allow elements with nested character data to be imported as properties, it is not always
+     * possible to determine whether the element represents a node or a property from within the {@code startElement} method.
+     * Therefore, {@code ElementEntries} are initially created in an {@link ElementEntryState#TBD unknown state} and submitted to
+     * the {@code destination} when it can be positively determined that the entry represents a property (if nested character data
+     * is encountered) or a node (if a child node is detected or the {@link XmlHandler#endElement(String, String, String)} method
+     * is invoked prior to encountering nested character data).
+     * </p>
+     * <p>
+     * As DNA does not currently support a way to add a value to an existing property through the Graph API, {@code
+     * ElementEntries} also contain a {@link Multimap} of property names to values. The node's properties are aggregated and only
+     * submitted to the {@code destination} when the {@link XmlHandler#endElement(String, String, String)} event fires.
+     * </p>
+     */
+    private class ElementEntry {
+
+        private ElementEntry parent;
+        // Stored separately since the root node has no parent ElementEntry but does have a path
+        private Path pathToParent;
+        private Path pathToThisNode;
+        private Name name;
+        private Multimap<Name, Object> properties;
+        private ElementEntryState state;
+
+        protected ElementEntry( ElementEntry parent,
+                                Path pathToParent,
+                                Name name ) {
+            this.parent = parent;
+            this.pathToParent = pathToParent;
+            this.name = name;
+            this.state = ElementEntryState.TBD;
+            properties = new LinkedHashMultimap<Name, Object>();
+        }
+
+        protected void setName( Name name ) {
+            this.name = name;
+            pathToThisNode = pathFactory.create(pathToParent, name);
+        }
+
+        protected void addProperty( Name propertyName,
+                                    Object propertyValue ) {
+            assert state != ElementEntryState.PROPERTY;
+            properties.put(propertyName, propertyValue);
+        }
+
+        protected void addAsNode() {
+            assert state != ElementEntryState.PROPERTY;
+            if (state == ElementEntryState.NODE) return;
+
+            state = ElementEntryState.NODE;
+            destination.create(pathFactory.create(pathToParent, name), Collections.<Property>emptyList());
+        }
+
+        protected void addAsPropertySetTo( Object value ) {
+            assert state != ElementEntryState.NODE;
+            state = ElementEntryState.PROPERTY;
+            parent.addProperty(name, value);
+        }
+
+        protected final Path path() {
+            return pathToThisNode;
+        }
+
+        protected void submit() {
+            if (state == ElementEntryState.PROPERTY) return;
+
+            if (state == ElementEntryState.NODE && properties.size() == 0) return;
+            Property[] propertiesToAdd = new Property[properties.size()];
+            int i = 0;
+            for (Name name : properties.keySet()) {
+                propertiesToAdd[i++] = createProperty(name, properties.get(name));
+            }
+
+            if (state == ElementEntryState.TBD) {
+                // Merge the add and the create
+                destination.create(pathToThisNode, Arrays.asList(propertiesToAdd));
+            } else {
+                destination.setProperties(pathToThisNode, propertiesToAdd);
+            }
+        }
+    }
 }

Modified: trunk/dna-graph/src/test/java/org/jboss/dna/graph/io/GraphImporterTest.java
===================================================================
--- trunk/dna-graph/src/test/java/org/jboss/dna/graph/io/GraphImporterTest.java	2009-04-30 15:31:52 UTC (rev 867)
+++ trunk/dna-graph/src/test/java/org/jboss/dna/graph/io/GraphImporterTest.java	2009-04-30 16:15:59 UTC (rev 868)
@@ -26,6 +26,7 @@
 import static org.hamcrest.core.Is.is;
 import static org.hamcrest.core.IsInstanceOf.instanceOf;
 import static org.junit.Assert.assertThat;
+import static org.junit.Assert.fail;
 import static org.mockito.Mockito.stub;
 import java.io.File;
 import java.net.URI;
@@ -51,6 +52,8 @@
 import org.jboss.dna.graph.request.CompositeRequest;
 import org.jboss.dna.graph.request.CreateNodeRequest;
 import org.jboss.dna.graph.request.Request;
+import org.jboss.dna.graph.request.SetPropertyRequest;
+import org.jboss.dna.graph.request.UpdatePropertiesRequest;
 import org.jboss.dna.graph.request.VerifyWorkspaceRequest;
 import org.junit.Before;
 import org.junit.Test;
@@ -98,8 +101,8 @@
         assertThat(lastExecutedRequest, is(instanceOf(CompositeRequest.class)));
         Iterator<Request> iter = ((CompositeRequest)lastExecutedRequest).iterator();
         // assertCreateNode(iter, "/a/b/", "jcr:primaryType={http://www.jboss.org/dna/xml/1.0}document");
-        assertCreateNode(iter, "/a/b/dna:system[1]", "jcr:primaryType={http://www.jcp.org/jcr/nt/1.0}unstructured");
-        assertCreateNode(iter, "/a/b/dna:system[1]/dna:sources[1]", "jcr:primaryType={http://www.jcp.org/jcr/nt/1.0}unstructured");
+        assertCreateNode(iter, "/a/b/dna:system[1]");
+        assertCreateNode(iter, "/a/b/dna:system[1]/dna:sources[1]");
         assertCreateNode(iter,
                          "/a/b/dna:system[1]/dna:sources[1]/sourceA[1]",
                          "repositoryName=repositoryA",
@@ -111,6 +114,8 @@
                          "repositoryName=repositoryB",
                          "jcr:primaryType={http://www.jcp.org/jcr/nt/1.0}unstructured",
                          "dna:classname=org.jboss.dna.connector.inmemory.InMemoryRepositorySource");
+        assertCreateProperties(iter, "/a/b/dna:system[1]/dna:sources[1]", "jcr:primaryType={http://www.jcp.org/jcr/nt/1.0}unstructured");
+        assertCreateProperties(iter, "/a/b/dna:system[1]", "jcr:primaryType={http://www.jcp.org/jcr/nt/1.0}unstructured");
         assertThat(iter.hasNext(), is(false));
     }
 
@@ -124,8 +129,60 @@
         Path parentPath = createNode.under().getPath();
         assertThat(parentPath, is(expectedPath.getParent()));
         assertThat(createNode.named(), is(expectedPath.getLastSegment().getName()));
+        
+        if (properties.length > 0) {
+            Map<Name, Property> propertiesByName = new HashMap<Name, Property>();
+            for (Property prop : createNode.properties()) {
+                propertiesByName.put(prop.getName(), prop);
+            }
+            for (String propertyStr : properties) {
+                if (propertyStr == "any properties") {
+                    propertiesByName.clear();
+                    break;
+                }
+                Matcher matcher = Pattern.compile("([^=]+)=(.*)").matcher(propertyStr);
+                if (!matcher.matches()) continue;
+                System.out.println("Property: " + propertyStr + " ==> " + matcher);
+                Name propertyName = context.getValueFactories().getNameFactory().create(matcher.group(1));
+                System.out.println("Property name: " + matcher.group(1));
+                String value = matcher.group(2); // doesn't handle multiple values!!
+                if (value.trim().length() == 0) value = null;
+                Property actual = propertiesByName.remove(propertyName);
+                Property expectedProperty = context.getPropertyFactory().create(propertyName, value);
+                assertThat("missing property " + propertyName, actual, is(expectedProperty));
+            }
+            if (!propertiesByName.isEmpty()) {
+                System.out.println("Properties for " + path + "\n" + propertiesByName);
+            }
+            assertThat(propertiesByName.isEmpty(), is(true));
+        }
+    }
+
+    public void assertCreateProperties( Iterator<Request> iterator,
+                                        String path,
+                                        String... properties ) {
+        Request nextCommand = iterator.next();
+        
+        if (nextCommand instanceof UpdatePropertiesRequest) {
+            assertUpdateProperties((UpdatePropertiesRequest) nextCommand, path, properties);
+        }
+        else if (nextCommand instanceof SetPropertyRequest) {
+            assertSetProperty((SetPropertyRequest) nextCommand, path, properties);
+        }
+        else {
+            fail("Invalid next request type: " + nextCommand.getClass().getName());
+        }
+        
+    }
+    public void assertUpdateProperties( UpdatePropertiesRequest createNode,
+                                  String path,
+                                  String... properties ) {
+        Path expectedPath = context.getValueFactories().getPathFactory().create(path);
+        Path parentPath = createNode.changedLocation().getPath().getParent();
+        assertThat(parentPath, is(expectedPath.getParent()));
+        assertThat(createNode.changedLocation().getPath().getLastSegment().getName(), is(expectedPath.getLastSegment().getName()));
         Map<Name, Property> propertiesByName = new HashMap<Name, Property>();
-        for (Property prop : createNode.properties()) {
+        for (Property prop : createNode.properties().values()) {
             propertiesByName.put(prop.getName(), prop);
         }
         for (String propertyStr : properties) {
@@ -150,6 +207,40 @@
         assertThat(propertiesByName.isEmpty(), is(true));
     }
 
+    public void assertSetProperty( SetPropertyRequest createNode,
+                                        String path,
+                                        String... properties ) {
+              Path expectedPath = context.getValueFactories().getPathFactory().create(path);
+              Path parentPath = createNode.changedLocation().getPath().getParent();
+              assertThat(parentPath, is(expectedPath.getParent()));
+              assertThat(createNode.changedLocation().getPath().getLastSegment().getName(), is(expectedPath.getLastSegment().getName()));
+              Map<Name, Property> propertiesByName = new HashMap<Name, Property>();
+              Property prop = createNode.property();
+              propertiesByName.put(prop.getName(), prop);
+
+              for (String propertyStr : properties) {
+                  if (propertyStr == "any properties") {
+                      propertiesByName.clear();
+                      break;
+                  }
+                  Matcher matcher = Pattern.compile("([^=]+)=(.*)").matcher(propertyStr);
+                  if (!matcher.matches()) continue;
+                  System.out.println("Property: " + propertyStr + " ==> " + matcher);
+                  Name propertyName = context.getValueFactories().getNameFactory().create(matcher.group(1));
+                  System.out.println("Property name: " + matcher.group(1));
+                  String value = matcher.group(2); // doesn't handle multiple values!!
+                  if (value.trim().length() == 0) value = null;
+                  Property actual = propertiesByName.remove(propertyName);
+                  Property expectedProperty = context.getPropertyFactory().create(propertyName, value);
+                  assertThat("missing property " + propertyName, actual, is(expectedProperty));
+              }
+              if (!propertiesByName.isEmpty()) {
+                  System.out.println("Properties for " + path + "\n" + propertiesByName);
+              }
+              assertThat(propertiesByName.isEmpty(), is(true));
+          }
+
+    
     protected class MockRepositoryConnection implements RepositoryConnection {
         public void close() {
         }

Modified: trunk/dna-graph/src/test/java/org/jboss/dna/graph/xml/XmlHandlerTest.java
===================================================================
--- trunk/dna-graph/src/test/java/org/jboss/dna/graph/xml/XmlHandlerTest.java	2009-04-30 15:31:52 UTC (rev 867)
+++ trunk/dna-graph/src/test/java/org/jboss/dna/graph/xml/XmlHandlerTest.java	2009-04-30 16:15:59 UTC (rev 868)
@@ -146,7 +146,11 @@
 
     @Test
     public void shouldParseXmlDocumentWithoutNamespaces() throws IOException, SAXException {
+        System.out.println("\n");
+        System.out.flush();
         parse("xmlHandler/docWithoutNamespaces.xml");
+        System.out.println("\n");
+        System.out.flush();
         // Check the generated content; note that the attribute name doesn't match, so the nodes don't get special names
         assertNode("Cars");
         assertNode("Cars/Hybrid");
@@ -350,14 +354,18 @@
         parse("xmlHandler/docWithoutNamespaces.xml");
         // Check the generated content; note that the attribute name doesn't match, so the nodes don't get special names
         String unstructPrimaryType = "jcr:primaryType={http://www.jcp.org/jcr/nt/1.0}unstructured";
-        assertNode("Cars", unstructPrimaryType);
-        assertNode("Cars/Hybrid", unstructPrimaryType);
+        assertNode("Cars");
+        assertNode("Cars/Hybrid");
         assertNode("Cars/Hybrid/car", unstructPrimaryType, "name=Toyota Prius", "maker=Toyota", "model=Prius");
         assertNode("Cars/Hybrid/car", unstructPrimaryType, "name=Toyota Highlander", "maker=Toyota", "model=Highlander");
         assertNode("Cars/Hybrid/car", unstructPrimaryType, "name=Nissan Altima", "maker=Nissan", "model=Altima");
-        assertNode("Cars/Sports", unstructPrimaryType);
+        assertProperties("Cars/Hybrid", unstructPrimaryType);
+        assertNode("Cars/Sports");
         assertNode("Cars/Sports/car", unstructPrimaryType, "name=Aston Martin DB9", "maker=Aston Martin", "model=DB9");
         assertNode("Cars/Sports/car", unstructPrimaryType, "name=Infiniti G37", "maker=Infiniti", "model=G37");
+        assertProperties("Cars/Sports", unstructPrimaryType);
+        assertProperties("Cars", unstructPrimaryType);
+        
     }
 
     @Test
@@ -371,21 +379,94 @@
         // Check the generated content; note that the attribute name doesn't match, so the nodes don't get special names
         String unstructPrimaryType = "jcr:primaryType={http://www.jcp.org/jcr/nt/1.0}unstructured";
         String carPrimaryType = "jcr:primaryType={http://default.namespace.com}car";
-        assertNode("c:Cars", unstructPrimaryType);
-        assertNode("c:Cars/c:Hybrid", unstructPrimaryType);
+        assertNode("c:Cars");
+        assertNode("c:Cars/c:Hybrid");
         assertNode("c:Cars/c:Hybrid/c:Toyota Prius", carPrimaryType, "c:maker=Toyota", "c:model=Prius");
         assertNode("c:Cars/c:Hybrid/c:Toyota Highlander", carPrimaryType, "c:maker=Toyota", "c:model=Highlander");
         assertNode("c:Cars/c:Hybrid/c:Nissan Altima", carPrimaryType, "c:maker=Nissan", "c:model=Altima");
-        assertNode("c:Cars/c:Sports", unstructPrimaryType);
+        assertProperties("c:Cars/c:Hybrid", unstructPrimaryType);
+        assertNode("c:Cars/c:Sports");
         assertNode("c:Cars/c:Sports/c:Aston Martin DB9", carPrimaryType, "c:maker=Aston Martin", "c:model=DB9");
         assertNode("c:Cars/c:Sports/c:Infiniti G37", carPrimaryType, "c:maker=Infiniti", "c:model=G37");
+        assertProperties("c:Cars/c:Sports", unstructPrimaryType);
+        assertProperties("c:Cars", unstructPrimaryType);
     }
 
+    @Test
+    public void shouldParseXmlDocumentWithNestedPropertiesShouldPlaceContentUnderRootNode() throws IOException, SAXException {
+        context.getNamespaceRegistry().register("jcr", "http://www.jcp.org/jcr/1.0");
+        handler = new XmlHandler(destination, skipRootElement, parentPath, decoder, nameAttribute, typeAttribute,
+                                 typeAttributeValue, scoping);
+        parse("xmlHandler/docWithNestedProperties.xml");
+        // Check the generated content; note that the attribute name DOES match, so the nodes names come from "jcr:name" attribute
+        assertNode("Cars");
+        assertNode("Cars/Hybrid");
+        assertNode("Cars/Hybrid/car", "name=Toyota Prius", "maker=Toyota", "model=Prius");
+        assertNode("Cars/Hybrid/car", "name=Toyota Highlander", "maker=Toyota", "model=Highlander");
+        assertNode("Cars/Hybrid/car");
+        assertProperties("Cars/Hybrid/car", "name=Nissan Altima", "maker=Nissan", "model=Altima");
+        assertNode("Cars/Sports");
+        assertNode("Cars/Sports/car", "name=Aston Martin DB9", "maker=Aston Martin", "model=DB9");
+        assertNode("Cars/Sports/car");
+        assertNode("Cars/Sports/car/driver", "name=Tony Stewart");
+        assertProperties("Cars/Sports/car", "name=Infiniti G37", "maker=Infiniti", "model=G37", "category=Turbocharged=My Sedan");
+        assertNode("Cars/Sports/car");
+        assertNode("Cars/Sports/car/jcr:xmltext", "jcr:xmlcharacters=This is my text ");        
+        assertNode("Cars/Sports/car/jcr:xmltext", "jcr:xmlcharacters=that should be merged");
+        assertProperties("Cars/Sports/car", "name=Infiniti G37", "maker=Infiniti", "model=G37");
+    }
+
     protected void assertNode( String path,
                                String... properties ) {
         // Create the expected path ...
         PathFactory factory = context.getValueFactories().getPathFactory();
         Path expectedPath = parentPath != null ? factory.create(parentPath, path) : factory.create("/" + path);
+        // Now get the next request and compare the expected and actual ...
+        CreateNodeRequest request = requests.remove();
+        Path parentPath = request.under().getPath();
+        assertThat(parentPath, is(expectedPath.getParent()));
+        assertThat(request.named(), is(expectedPath.getLastSegment().getName()));
+
+        if (properties.length != 0) {
+            // Create the list of properties ...
+            Map<Name, Property> expectedProperties = new HashMap<Name, Property>();
+            for (String propertyString : properties) {
+                String[] strings = propertyString.split("=");
+                if (strings.length < 2) continue;
+                Name name = context.getValueFactories().getNameFactory().create(strings[0]);
+                Object[] values = new Object[strings.length - 1];
+                for (int i = 1; i != strings.length; ++i) {
+                    values[i - 1] = strings[i];
+                }
+                Property property = context.getPropertyFactory().create(name, values);
+                expectedProperties.put(name, property);
+            }
+
+            for (Property actual : request.properties()) {
+                Property expected = expectedProperties.remove(actual.getName());
+                assertThat("unexpected property: " + actual, expected, is(notNullValue()));
+                assertThat(actual, is(expected));
+            }
+            if (!expectedProperties.isEmpty()) {
+                StringBuilder msg = new StringBuilder("missing actual properties: ");
+                boolean isFirst = true;
+                for (Property expected : expectedProperties.values()) {
+                    if (!isFirst) msg.append(", ");
+                    else isFirst = false;
+                    msg.append(expected.getName());
+                }
+                msg.append(" on node ").append(request.under());
+                System.out.println("Found properties: " + request.properties());
+                assertThat(msg.toString(), expectedProperties.isEmpty(), is(true));
+            }
+        }
+    }
+
+    protected void assertProperties( String path,
+                               String... properties ) {
+        // Create the expected path ...
+        PathFactory factory = context.getValueFactories().getPathFactory();
+        Path expectedPath = parentPath != null ? factory.create(parentPath, path) : factory.create("/" + path);
         // Create the list of properties ...
         Map<Name, Property> expectedProperties = new HashMap<Name, Property>();
         for (String propertyString : properties) {
@@ -399,12 +480,13 @@
             Property property = context.getPropertyFactory().create(name, values);
             expectedProperties.put(name, property);
         }
-        // Now get the next request and compare the expected and actual ...
-        CreateNodeRequest request = requests.remove();
-        Path parentPath = request.under().getPath();
+
+        CreateNodeRequest propertyRequest = requests.remove();
+        Path parentPath = propertyRequest.under().getPath();
         assertThat(parentPath, is(expectedPath.getParent()));
-        assertThat(request.named(), is(expectedPath.getLastSegment().getName()));
-        for (Property actual : request.properties()) {
+        assertThat(propertyRequest.named(), is(expectedPath.getLastSegment().getName()));
+
+        for (Property actual : propertyRequest.properties()) {
             Property expected = expectedProperties.remove(actual.getName());
             assertThat("unexpected property: " + actual, expected, is(notNullValue()));
             assertThat(actual, is(expected));
@@ -417,6 +499,8 @@
                 else isFirst = false;
                 msg.append(expected.getName());
             }
+            msg.append(" on node ").append(propertyRequest.under());
+            System.out.println("Found properties: " + propertyRequest.properties());
             assertThat(msg.toString(), expectedProperties.isEmpty(), is(true));
         }
     }
@@ -481,6 +565,21 @@
             }
         }
 
+        public void setProperties( Path path, 
+                                   Property... properties ) {
+            if (properties.length == 0) {
+                return;
+            }
+            else if (properties.length == 1) {
+                create(path, properties[0]);
+            }
+            else {
+               Property[] additionalProperties = new Property[properties.length - 1];
+               System.arraycopy(properties, 1, additionalProperties, 0, properties.length - 1);
+               create(path, properties[0], additionalProperties);
+            }
+        }        
+        
         @SuppressWarnings( "synthetic-access" )
         public ExecutionContext getExecutionContext() {
             return XmlHandlerTest.this.context;

Added: trunk/dna-graph/src/test/resources/xmlHandler/docWithNestedProperties.xml
===================================================================
--- trunk/dna-graph/src/test/resources/xmlHandler/docWithNestedProperties.xml	                        (rev 0)
+++ trunk/dna-graph/src/test/resources/xmlHandler/docWithNestedProperties.xml	2009-04-30 16:15:59 UTC (rev 868)
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Cars xmlns:jcr="http://www.jcp.org/jcr/1.0">
+    <Hybrid>
+        <car name="Toyota Prius" maker="Toyota" model="Prius"/>
+        <car name="Toyota Highlander" maker="Toyota" model="Highlander"/>
+        <car name="Nissan Altima">
+            <property jcr:name="maker">Nissan</property>
+            <model>Altima</model>
+        </car>
+    </Hybrid>
+    <Sports>
+        <car name="Aston Martin DB9" maker="Aston Martin" model="DB9"/>
+        <car name="Infiniti G37">
+            <maker>Infiniti</maker>
+            <category>Turbocharged</category>
+            <driver name="Tony Stewart" />
+            <property jcr:name="model">G37</property>
+            <category>My <![CDATA[Sedan]]></category>
+        </car>
+        <car name="Infiniti G37" maker="Infiniti" model="G37">
+	       <unstructured jcr:name="jcr:xmltext" jcr:xmlcharacters="This is my text " />
+	       <unstructured jcr:name="jcr:xmltext" jcr:xmlcharacters="that should be merged" />        
+        </car>
+    </Sports>
+</Cars>
\ No newline at end of file


Property changes on: trunk/dna-graph/src/test/resources/xmlHandler/docWithNestedProperties.xml
___________________________________________________________________
Name: svn:mime-type
   + text/plain

Modified: trunk/extensions/dna-sequencer-cnd/src/main/java/org/jboss/dna/sequencer/cnd/CndSequencer.java
===================================================================
--- trunk/extensions/dna-sequencer-cnd/src/main/java/org/jboss/dna/sequencer/cnd/CndSequencer.java	2009-04-30 15:31:52 UTC (rev 867)
+++ trunk/extensions/dna-sequencer-cnd/src/main/java/org/jboss/dna/sequencer/cnd/CndSequencer.java	2009-04-30 16:15:59 UTC (rev 868)
@@ -111,6 +111,19 @@
         /**
          * {@inheritDoc}
          * 
+         * @see org.jboss.dna.graph.io.Destination#setProperties(org.jboss.dna.graph.property.Path,
+         *      org.jboss.dna.graph.property.Property[])
+         */
+        public void setProperties( Path path,
+                                   Property... properties ) {
+            for (Property property : properties) {
+                output.setProperty(path, property.getName(), property.getValues());
+            }
+        }
+
+        /**
+         * {@inheritDoc}
+         * 
          * @see org.jboss.dna.graph.io.Destination#submit()
          */
         public void submit() {




More information about the dna-commits mailing list