test harness for L1 text handling

Signed-off-by: Florian Thienel <florian@thienel.org>
diff --git a/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/internal/dom/DummyValidator.java b/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/internal/dom/DummyValidator.java
new file mode 100644
index 0000000..64503c9
--- /dev/null
+++ b/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/internal/dom/DummyValidator.java
@@ -0,0 +1,53 @@
+/*******************************************************************************

+ * Copyright (c) 2012 Florian Thienel and others.

+ * All rights reserved. This program and the accompanying materials

+ * are made available under the terms of the Eclipse Public License v1.0

+ * which accompanies this distribution, and is available at

+ * http://www.eclipse.org/legal/epl-v10.html

+ * 

+ * Contributors:

+ * 		Florian Thienel - initial API and implementation

+ *******************************************************************************/

+package org.eclipse.vex.core.internal.dom;

+

+import java.util.Collections;

+import java.util.List;

+import java.util.Set;

+

+import org.eclipse.core.runtime.QualifiedName;

+import org.eclipse.vex.core.internal.validator.AttributeDefinition;

+

+/**

+ * @author Florian Thienel

+ */

+public class DummyValidator implements Validator {

+

+	public AttributeDefinition getAttributeDefinition(final Attribute attribute) {

+		return null;

+	}

+

+	public List<AttributeDefinition> getAttributeDefinitions(final Element element) {

+		return Collections.emptyList();

+	}

+

+	public Set<QualifiedName> getValidRootElements() {

+		return Collections.emptySet();

+	}

+

+	public Set<QualifiedName> getValidItems(final Element element) {

+		return Collections.emptySet();

+	}

+

+	public boolean isValidSequence(final QualifiedName element, final List<QualifiedName> nodes, final boolean partial) {

+		return false;

+	}

+

+	public boolean isValidSequence(final QualifiedName element, final List<QualifiedName> sequence1, final List<QualifiedName> sequence2, final List<QualifiedName> sequence3, final boolean partial) {

+		return false;

+	}

+

+	public Set<String> getRequiredNamespaces() {

+		return Collections.emptySet();

+	}

+

+}

diff --git a/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/internal/dom/L1TextHandlingTest.java b/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/internal/dom/L1TextHandlingTest.java
new file mode 100644
index 0000000..feda08b
--- /dev/null
+++ b/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/internal/dom/L1TextHandlingTest.java
@@ -0,0 +1,91 @@
+/*******************************************************************************

+ * Copyright (c) 2012 Florian Thienel and others.

+ * All rights reserved. This program and the accompanying materials

+ * are made available under the terms of the Eclipse Public License v1.0

+ * which accompanies this distribution, and is available at

+ * http://www.eclipse.org/legal/epl-v10.html

+ * 

+ * Contributors:

+ * 		Florian Thienel - initial API and implementation

+ *******************************************************************************/

+package org.eclipse.vex.core.internal.dom;

+

+import static org.junit.Assert.assertEquals;

+import static org.junit.Assert.assertFalse;

+import static org.junit.Assert.assertTrue;

+

+import java.util.List;

+

+import org.eclipse.core.runtime.AssertionFailedException;

+import org.eclipse.core.runtime.QualifiedName;

+import org.junit.Before;

+import org.junit.Test;

+

+/**

+ * @author Florian Thienel

+ */

+public class L1TextHandlingTest {

+

+	private Document document;

+	private Element titleElement;

+

+	@Before

+	public void setUp() throws Exception {

+		document = new Document(new Element("root"));

+		titleElement = new Element("title");

+		document.insertElement(1, titleElement);

+		document.setValidator(new DummyValidator() {

+			@Override

+			public boolean isValidSequence(final QualifiedName element, final List<QualifiedName> nodes, final boolean partial) {

+				return "title".equals(element.getLocalName());

+			}

+

+			@Override

+			public boolean isValidSequence(final QualifiedName element, final List<QualifiedName> sequence1, final List<QualifiedName> sequence2, final List<QualifiedName> sequence3,

+					final boolean partial) {

+				return "title".equals(element.getLocalName());

+			}

+		});

+	}

+

+	@Test

+	public void shouldIndicateValidTextInsertionPoints() throws Exception {

+		assertFalse("the root element may not contain textual contant", document.canInsertText(titleElement.getStartOffset() - 1));

+		assertFalse("the start offset is the last insertion point of the root element before the title element", document.canInsertText(titleElement.getStartOffset()));

+		assertTrue("append new content before the end offset of the title element", document.canInsertText(titleElement.getEndOffset()));

+		assertFalse("the root element may not contain textual contant", document.canInsertText(titleElement.getEndOffset() + 1));

+	}

+

+	@Test

+	public void insertTextAtValidInsertionPoint() throws Exception {

+		document.insertText(titleElement.getEndOffset(), "Hello World");

+		assertEquals("Hello World", titleElement.getText());

+	}

+

+	@Test(expected = AssertionFailedException.class)

+	public void cannotInsertTextBeforeDocumentStart() throws Exception {

+		document.insertText(-1, "Hello World");

+	}

+

+	@Test(expected = AssertionFailedException.class)

+	public void cannotInsertTextAtDocumentStartOffset() throws Exception {

+		document.insertText(0, "Hello World");

+	}

+

+	@Test(expected = AssertionFailedException.class)

+	public void cannotInsertTextAfterDocumentEndOffset() throws Exception {

+		document.insertText(document.getEndOffset() + 1, "Hello World");

+	}

+

+	@Test(expected = DocumentValidationException.class)

+	public void cannotInsertTextAtInvalidInsertionPoint() throws Exception {

+		// titleElement.startOffset is the last insertion point in root before title; root may not contain text according to the validator

+		document.insertText(titleElement.getStartOffset(), "Hello World");

+	}

+

+	@Test

+	public void shouldConvertControlCharactersToSpaces() throws Exception {

+		document.insertText(titleElement.getEndOffset(), "\0\u001F\n");

+		assertEquals("control characters except \\n are converted to spaces", "  \n", titleElement.getText());

+	}

+}

diff --git a/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/tests/VEXCoreTestSuite.java b/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/tests/VEXCoreTestSuite.java
index 39f94a5..2df93dd 100644
--- a/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/tests/VEXCoreTestSuite.java
+++ b/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/tests/VEXCoreTestSuite.java
@@ -29,6 +29,7 @@
 import org.eclipse.vex.core.internal.dom.DocumentTest;
 import org.eclipse.vex.core.internal.dom.DocumentWriterTest;
 import org.eclipse.vex.core.internal.dom.GapContentTest;
+import org.eclipse.vex.core.internal.dom.L1TextHandlingTest;
 import org.eclipse.vex.core.internal.dom.NamespaceStackTest;
 import org.eclipse.vex.core.internal.dom.NamespaceTest;
 import org.eclipse.vex.core.internal.dom.ParentTest;
@@ -62,6 +63,7 @@
 		addTest(new JUnit4TestAdapter(BasicNodeTest.class));
 		addTest(new JUnit4TestAdapter(ParentTest.class));
 		addTest(new JUnit4TestAdapter(DocumentTest.class));
+		addTest(new JUnit4TestAdapter(L1TextHandlingTest.class));
 		addTest(new JUnit4TestAdapter(DocumentFragmentTest.class));
 		addTestSuite(PropertyTest.class);
 		addTestSuite(RuleTest.class);
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/dom/Document.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/dom/Document.java
index def8e79..3cd7bb7 100644
--- a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/dom/Document.java
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/dom/Document.java
@@ -160,33 +160,72 @@
 		return getContent().length();
 	}
 
-	private boolean canInsertAt(final int offset, final QualifiedName... nodeNames) {
-		return canInsertAt(offset, Arrays.asList(nodeNames));
+	public Position createPosition(final int offset) {
+		return getContent().createPosition(offset);
 	}
 
-	private boolean canInsertAt(final int offset, final List<QualifiedName> nodeNames) {
+	private boolean canInsertAt(final Element insertionParent, final int offset, final QualifiedName... nodeNames) {
+		return canInsertAt(insertionParent, offset, Arrays.asList(nodeNames));
+	}
+
+	private boolean canInsertAt(final Element insertionParent, final int offset, final List<QualifiedName> nodeNames) {
 		if (validator == null) {
 			return true;
 		}
 
-		final Element parent = getElementAt(offset);
-		final List<QualifiedName> seq1 = getNodeNames(parent.getChildNodesBefore(offset));
-		final List<QualifiedName> seq2 = nodeNames;
-		final List<QualifiedName> seq3 = getNodeNames(parent.getChildNodesAfter(offset));
+		if (insertionParent == null) {
+			return false;
+		}
 
-		return validator.isValidSequence(parent.getQualifiedName(), seq1, seq2, seq3, true);
+		final List<QualifiedName> prefix = getNodeNames(insertionParent.getChildNodesBefore(offset));
+		final List<QualifiedName> insertionCandidates = nodeNames;
+		final List<QualifiedName> suffix = getNodeNames(insertionParent.getChildNodesAfter(offset));
+
+		return validator.isValidSequence(insertionParent.getQualifiedName(), prefix, insertionCandidates, suffix, true);
 	}
 
-	public boolean canInsertFragment(final int offset, final DocumentFragment fragment) {
-		return canInsertAt(offset, fragment.getNodeNames());
+	private Element getInsertionParentAt(final int offset) {
+		final Element parent = getElementAt(offset);
+		if (offset == parent.getStartOffset()) {
+			return parent.getParentElement();
+		}
+		return parent;
 	}
 
 	public boolean canInsertText(final int offset) {
-		return canInsertAt(offset, Validator.PCDATA);
+		return canInsertAt(getInsertionParentAt(offset), offset, Validator.PCDATA);
 	}
 
-	public Position createPosition(final int offset) {
-		return getContent().createPosition(offset);
+	public void insertText(final int offset, final String text) throws DocumentValidationException {
+		Assert.isTrue(offset > getStartOffset() && offset <= getEndOffset(), MessageFormat.format("Offset must be in [{0}, {1}]", getStartOffset() + 1, getEndOffset()));
+
+		final Element parent = getInsertionParentAt(offset);
+		if (!canInsertAt(parent, offset, Validator.PCDATA)) {
+			throw new DocumentValidationException(MessageFormat.format("Cannot insert text ''{0}'' at offset {1}.", text, offset));
+		}
+
+		final String adjustedText = convertControlCharactersToSpaces(text);
+
+		fireBeforeContentInserted(new DocumentEvent(this, parent, offset, 2, null));
+
+		getContent().insertText(offset, adjustedText);
+
+		final IUndoableEdit edit = undoEnabled ? new InsertTextEdit(offset, adjustedText) : null;
+		fireContentInserted(new DocumentEvent(this, parent, offset, adjustedText.length(), edit));
+	}
+
+	private String convertControlCharactersToSpaces(final String text) {
+		final char[] characters = text.toCharArray();
+		for (int i = 0; i < characters.length; i++) {
+			if (Character.isISOControl(characters[i]) && characters[i] != '\n') {
+				characters[i] = ' ';
+			}
+		}
+		return new String(characters);
+	}
+
+	public boolean canInsertFragment(final int offset, final DocumentFragment fragment) {
+		return canInsertAt(getInsertionParentAt(offset), offset, fragment.getNodeNames());
 	}
 
 	public void delete(final Range range) throws DocumentValidationException {
@@ -369,7 +408,7 @@
 			throw new IllegalArgumentException("Error inserting element <" + element.getPrefixedName() + ">: offset is " + offset + ", but it must be between 1 and " + (getLength() - 1));
 		}
 
-		if (!canInsertAt(offset, element.getQualifiedName())) {
+		if (!canInsertAt(getInsertionParentAt(offset), offset, element.getQualifiedName())) {
 			throw new DocumentValidationException("Cannot insert element " + element.getPrefixedName() + " at offset " + offset);
 		}
 
@@ -416,7 +455,7 @@
 			throw new IllegalArgumentException("Error inserting document fragment");
 		}
 
-		if (!canInsertAt(offset, fragment.getNodeNames())) {
+		if (!canInsertAt(getInsertionParentAt(offset), offset, fragment.getNodeNames())) {
 			throw new DocumentValidationException("Cannot insert document fragment");
 		}
 
@@ -444,43 +483,6 @@
 		fireContentInserted(new DocumentEvent(this, parent, offset, fragment.getContent().length(), edit));
 	}
 
-	public void insertText(final int offset, final String text) throws DocumentValidationException {
-		if (offset < 1 || offset >= getLength()) {
-			throw new IllegalArgumentException("Offset must be between 1 and n-1");
-		}
-
-		final boolean isValid;
-		if (!getContent().isElementMarker(offset - 1)) {
-			isValid = true;
-		} else if (!getContent().isElementMarker(offset)) {
-			isValid = true;
-		} else {
-			isValid = canInsertAt(offset, Validator.PCDATA);
-		}
-
-		if (!isValid) {
-			throw new DocumentValidationException("Cannot insert text '" + text + "' at offset " + offset);
-		}
-
-		// Convert control chars to spaces
-		final StringBuffer sb = new StringBuffer(text);
-		for (int i = 0; i < sb.length(); i++) {
-			if (Character.isISOControl(sb.charAt(i)) && sb.charAt(i) != '\n') {
-				sb.setCharAt(i, ' ');
-			}
-		}
-		final String s = sb.toString();
-
-		final Element parent = getElementAt(offset);
-		fireBeforeContentInserted(new DocumentEvent(this, parent, offset, 2, null));
-
-		getContent().insertText(offset, s);
-
-		final IUndoableEdit edit = undoEnabled ? new InsertTextEdit(offset, s) : null;
-
-		fireContentInserted(new DocumentEvent(this, parent, offset, s.length(), edit));
-	}
-
 	public void fireAttributeChanged(final DocumentEvent e) {
 		listeners.fireEvent("attributeChanged", e);
 	}
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/dom/Parent.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/dom/Parent.java
index b51ba4f..ae30ac0 100644
--- a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/dom/Parent.java
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/dom/Parent.java
@@ -10,6 +10,7 @@
  *******************************************************************************/

 package org.eclipse.vex.core.internal.dom;

 

+import java.text.MessageFormat;

 import java.util.ArrayList;

 import java.util.Collections;

 import java.util.Iterator;

@@ -181,7 +182,7 @@
 	 * @return the node at the given offset

 	 */

 	public Node getChildNodeAt(final int offset) {

-		Assert.isTrue(containsOffset(offset));

+		Assert.isTrue(containsOffset(offset), MessageFormat.format("Offset must be within {0}.", getRange()));

 		final List<Node> childNodes = getChildNodes();

 		for (final Node child : childNodes) {

 			if (child.containsOffset(offset)) {