Skip to content
Browse files

Fixes #65 : Add sort logic to Element (and ContentList).

Move sorting from Contrib - this is not a straight port, rather, it is the implementation of the same core functionality, but using all the nice generics tricks (and some not-so-nice ones).
  • Loading branch information...
1 parent ab7f9c4 commit 529a1f69506376951816a1fa37e912e14d7ddfbb @rolfl rolfl committed Feb 22, 2012
View
113 core/src/java/org/jdom2/ContentList.java
@@ -115,7 +115,7 @@ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
ContentList(final Parent parent) {
this.parent = parent;
}
-
+
/**
* Package internal method to support building from sources that are 100%
* trusted.
@@ -555,13 +555,74 @@ public int size() {
/**
* Return this list as a <code>String</code>
*
- * @return The number of items in this list.
+ * @return The String representation of this list.
*/
@Override
public String toString() {
return super.toString();
}
+
+ void sortInPlace(final int[] indexes) {
+ // the indexes are a discrete set of values that have no duplicates,
+ // and describe the relative order of each of them.
+ // as a result, we can do some tricks....
+ final int[] unsorted = ArrayCopy.copyOf(indexes, indexes.length);
+ Arrays.sort(unsorted);
+ final Content[] usc = new Content[unsorted.length];
+ for (int i = 0; i < usc.length; i++) {
+ usc[i] = elementData[indexes[i]];
+ }
+ // usc contains the content in their pre-sorted order....
+ for (int i = 0; i < indexes.length; i ++) {
+ elementData[unsorted[i]] = usc[i];
+ }
+ }
+ /**
+ * Unlike the Arrays.binarySearch, this method never expects an
+ * "already exists" condition, we only ever add, thus there will never
+ * be a negative insertion-point.
+ * @param indexes THe pointers to search within
+ * @param len The number of pointers to search within
+ * @param val The pointer we are checking for.
+ * @param comp The Comparator to compare with
+ * @return the insertion point.
+ */
+ private final int binarySearch(final int[] indexes, final int len,
+ final int val, final Comparator<? super Content> comp) {
+ int left = 0, mid = 0, right = len - 1, cmp = 0;
+ final Content base = elementData[val];
+ while (left <= right) {
+ mid = (left + right) >>> 1;
+ cmp = comp.compare(base, elementData[indexes[mid]]);
+ if (cmp == 0) {
+ while (cmp == 0 && mid < right && comp.compare(
+ base, elementData[indexes[mid + 1]]) == 0) {
+ mid++;
+ }
+ return mid + 1;
+ } else if (cmp < 0) {
+ right = mid - 1;
+ } else {
+ left = mid + 1;
+ }
+ }
+ return left;
+ }
+
+ final void sort(final Comparator<? super Content> comp) {
+ final int sz = size;
+ int[] indexes = new int[sz];
+ for (int i = 0 ; i < sz; i++) {
+ final int ip = binarySearch(indexes, i, i, comp);
+ if (ip < i) {
+ System.arraycopy(indexes, ip, indexes, ip+1, i - ip);
+ }
+ indexes[ip] = i;
+ }
+ sortInPlace(indexes);
+ }
+
/* * * * * * * * * * * * * ContentListIterator * * * * * * * * * * * * * * * */
/* * * * * * * * * * * * * ContentListIterator * * * * * * * * * * * * * * * */
/**
@@ -1126,6 +1187,54 @@ public int size() {
return backingsize;
}
+ /**
+ * Unlike the Arrays.binarySearch, this method never expects an
+ * "already exists" condition, we only ever add, thus there will never
+ * be a negative insertion-point.
+ * @param indexes THe pointers to search within
+ * @param len The number of pointers to search within
+ * @param val The pointer we are checking for.
+ * @param comp The Comparator to compare with
+ * @return the insertion point.
+ */
+ @SuppressWarnings("unchecked")
+ private final int fbinarySearch(final int[] indexes, final int len,
+ final int val, final Comparator<? super F> comp) {
+ int left = 0, mid = 0, right = len - 1, cmp = 0;
+ final F base = (F)elementData[backingpos[val]];
+ while (left <= right) {
+ mid = (left + right) >>> 1;
+ cmp = comp.compare(base, (F)elementData[indexes[mid]]);
+ if (cmp == 0) {
+ while (cmp == 0 && mid < right && comp.compare(
+ base, (F)elementData[indexes[mid + 1]]) == 0) {
+ mid++;
+ }
+ return mid + 1;
+ } else if (cmp < 0) {
+ right = mid - 1;
+ } else {
+ left = mid + 1;
+ }
+ }
+ return left;
+ }
+
+
+ final void sort(final Comparator<? super F> comp) {
+ // this size() forces a full scan/update of the list.
+ final int sz = size();
+ final int[] indexes = new int[sz];
+ for (int i = 0 ; i < sz; i++) {
+ final int ip = fbinarySearch(indexes, i, i, comp);
+ if (ip < i) {
+ System.arraycopy(indexes, ip, indexes, ip+1, i - ip);
+ }
+ indexes[ip] = backingpos[i];
+ }
+ sortInPlace(indexes);
+ }
+
}
/* * * * * * * * * * * * * FilterListIterator * * * * * * * * * * * */
View
81 core/src/java/org/jdom2/Element.java
@@ -63,11 +63,13 @@ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
+import java.util.Comparator;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.TreeMap;
+import org.jdom2.ContentList.FilterList;
import org.jdom2.filter.ElementFilter;
import org.jdom2.filter.Filter;
@@ -1784,6 +1786,85 @@ public void canContainContent(Content child, int index, boolean replace) throws
"A DocType is not allowed except at the document level");
}
}
+
+ /**
+ * Sort the contents of this Element using a mechanism that is safe for JDOM
+ * content. See the notes on {@link #sortContent(Filter, Comparator)} for
+ * how the algorithm works.
+ * <p>
+ * {@link Collections#sort(List, Comparator)} is not appropriate for sorting
+ * the Lists returned from {@link Element#getContent()} because those are
+ * 'live' lists, and the Collections.sort() method uses an algorithm that
+ * adds the content in the new location before removing it from the old.
+ * That creates validation issues with content attempting to attach to a
+ * parent before detaching first.
+ * <p>
+ * This method provides a safe means to conveniently sort the content.
+ *
+ * @param comparator The Comparator to use for the sorting.
+ */
+ public void sortContent(Comparator<? super Content> comparator) {
+ content.sort(comparator);
+ }
+
+ /**
+ * Sort the child Elements of this Element using a mechanism that is safe
+ * for JDOM content. Other child content will be unaffected. See the notes
+ * on {@link #sortContent(Filter, Comparator)} for how the algorithm works.
+ * <p>
+ * {@link Collections#sort(List, Comparator)} is not appropriate for sorting
+ * the Lists returned from {@link Element#getContent()} because those are
+ * 'live' lists, and the Collections.sort() method uses an algorithm that
+ * adds the content in the new location before removing it from the old.
+ * This creates validation issues with content attempting to attach to a
+ * parent before detaching first.
+ * <p>
+ * This method provides a safe means to conveniently sort the content.
+ *
+ * @param comparator The Comparator to use for the sorting.
+ */
+ public void sortChildren(Comparator <? super Element> comparator) {
+ ((FilterList<Element>)getChildren()).sort(comparator);
+ }
+
+ /**
+ * Sort the child content of this Element that matches the Filter, using a
+ * mechanism that is safe for JDOM content. Other child content (that does
+ * not match the filter) will be unaffected.
+ * <p>
+ * The algorithm used for sorting affects the child content in the following
+ * ways:
+ * <ol>
+ * <li>Items not matching the Filter will be unchanged. This includes the
+ * absolute position of that content in this Element. i.e. if child content
+ * <code>cc</code> does not match the Filter, then <code>indexOf(cc)</code>
+ * will not be changed by this sort.
+ * <li>Items matching the Filter will be reordered according to the
+ * comparator. Only the relative order of the Filtered data will change.
+ * <li>Items that compare as 'equals' using the comparator will keep the
+ * the same relative order as before the sort.
+ * </ol>
+ * <p>
+ * {@link Collections#sort(List, Comparator)} is not appropriate for sorting
+ * the Lists returned from {@link Element#getContent()} because those are
+ * 'live' lists, and the Collections.sort() method uses an algorithm that
+ * adds the content in the new location before removing it from the old.
+ * This creates validation issues with content attempting to attach to a
+ * parent before detaching first.
+ * <p>
+ * This method provides a safe means to conveniently sort the content.
+ * @param <E> The generic type of the Filter used to select the content to
+ * sort.
+ * @param filter The Filter used to select which child content to sort.
+ * @param comparator The Comparator to use for the sorting.
+ */
+ public <E extends Content> void sortContent(Filter<E> filter, Comparator <? super E> comparator) {
+ final FilterList<E> list = (FilterList<E>)getContent(filter);
+ list.sort(comparator);
+
+ }
+
+
/**
View
246 test/src/java/org/jdom2/test/cases/TestElement.java
@@ -75,6 +75,7 @@ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
+import java.util.Comparator;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
@@ -96,8 +97,10 @@ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
import org.jdom2.Namespace;
import org.jdom2.ProcessingInstruction;
import org.jdom2.Text;
+import org.jdom2.Content.CType;
import org.jdom2.filter.ContentFilter;
import org.jdom2.filter.ElementFilter;
+import org.jdom2.filter.Filters;
import org.jdom2.output.XMLOutputter;
import org.jdom2.test.util.UnitTestUtil;
@@ -2558,4 +2561,247 @@ public void testCloneDetatchParentElement() {
public void testContentCType() {
assertTrue(Content.CType.Element == new Element("root").getCType());
}
+
+ private final Comparator<Text> alphaText = new Comparator<Text>() {
+ @Override
+ public int compare(Text o1, Text o2) {
+ return o1.getText().compareTo(o2.getText());
+ }
+ };
+
+ private final Comparator<Content> alphaContent = new Comparator<Content>() {
+ @Override
+ public int compare(Content o1, Content o2) {
+ final int ctd = o1.getCType().ordinal() - o2.getCType().ordinal();
+ if (ctd != 0) {
+ return ctd;
+ }
+ final CType ct = o1.getCType();
+ switch (ct) {
+ case Comment:
+ return ((Comment)o1).getText().compareTo(((Comment)o2).getText());
+ case CDATA:
+ return ((CDATA)o1).getText().compareTo(((CDATA)o2).getText());
+ case DocType:
+ return ((DocType)o1).getElementName()
+ .compareTo(((DocType)o2).getElementName());
+ case Element:
+ return ((Element)o1).getName()
+ .compareTo(((Element)o2).getName());
+ case ProcessingInstruction:
+ return ((ProcessingInstruction)o1).getTarget()
+ .compareTo(((ProcessingInstruction)o2).getTarget());
+ case EntityRef:
+ return ((EntityRef)o1).getName()
+ .compareTo(((EntityRef)o2).getName());
+ case Text:
+ return ((Text)o1).getText().compareTo(((Text)o2).getText());
+ }
+ return 0;
+ }
+ };
+
+ @Test
+ public void testSort() {
+ Element emt = new Element("root");
+ emt.addContent(new Text("d"));
+ emt.addContent(new Text("c"));
+ emt.addContent(new Text("b"));
+ emt.addContent(new Text("a"));
+ assertEquals("dcba", emt.getText());
+
+ emt.sortContent(Filters.text(), alphaText);
+
+ assertEquals("abcd", emt.getText());
+ }
+
+ @Test
+ public void testSortOnlyContent() {
+ Element emt = new Element("root");
+ emt.addContent(new Text("a"));
+ assertEquals("a", emt.getText());
+ emt.sortContent(alphaContent);
+ assertEquals("a", emt.getText());
+ }
+
+ @Test
+ public void testSortOnlyFiltered() {
+ Element emt = new Element("root");
+ emt.addContent(new Text("a"));
+ assertEquals("a", emt.getText());
+ emt.sortContent(Filters.text(), alphaText);
+ assertEquals("a", emt.getText());
+ }
+
+ @Test
+ public void testSortAllSameContent() {
+ Element emt = new Element("root");
+ emt.addContent(new Text("a"));
+ emt.addContent(new Text("a"));
+ emt.addContent(new Text("a"));
+ emt.addContent(new Text("a"));
+ assertEquals("aaaa", emt.getText());
+ emt.sortContent(alphaContent);
+ assertEquals("aaaa", emt.getText());
+ }
+
+ @Test
+ public void testSortAllSameFiltered() {
+ Element emt = new Element("root");
+ emt.addContent(new Text("a"));
+ emt.addContent(new Text("a"));
+ emt.addContent(new Text("a"));
+ emt.addContent(new Text("a"));
+ assertEquals("aaaa", emt.getText());
+ emt.sortContent(Filters.text(), alphaText);
+ assertEquals("aaaa", emt.getText());
+ }
+
+ @Test
+ public void testSortContent() {
+ Element emt = new Element("root");
+ CDATA cdata = new CDATA("XXX");
+ emt.addContent(new Text("d"));
+ emt.addContent(cdata);
+ emt.addContent(new Text("c"));
+ emt.addContent(new Text("b"));
+ emt.addContent(new Text("a"));
+ assertEquals("dXXXcba", emt.getText());
+
+ emt.sortContent(alphaContent);
+
+ assertEquals("abcdXXX", emt.getText());
+ assertEquals(cdata, emt.getContent(4));
+ }
+
+ @Test
+ public void testSortElementContent() {
+ final Element emt = new Element("root");
+ final CDATA cdata = new CDATA("XXX");
+ final Element a = new Element("a");
+ final Element b = new Element("b");
+ final Element c = new Element("c");
+ final Element d = new Element("d");
+
+ emt.addContent(d);
+ emt.addContent(cdata);
+ emt.addContent(c);
+ emt.addContent(b);
+ emt.addContent(a);
+
+ assertTrue(emt.getContent(0) == d);
+ assertTrue(emt.getContent(1) == cdata);
+ assertTrue(emt.getContent(2) == c);
+ assertTrue(emt.getContent(3) == b);
+ assertTrue(emt.getContent(4) == a);
+
+ emt.sortChildren(alphaContent);
+ assertTrue(emt.getContent(0) == a);
+ assertTrue(emt.getContent(1) == cdata);
+ assertTrue(emt.getContent(2) == b);
+ assertTrue(emt.getContent(3) == c);
+ assertTrue(emt.getContent(4) == d);
+
+ emt.sortContent(alphaContent);
+
+ assertTrue(emt.getContent(0) == a);
+ assertTrue(emt.getContent(1) == b);
+ assertTrue(emt.getContent(2) == c);
+ assertTrue(emt.getContent(3) == d);
+ assertTrue(emt.getContent(4) == cdata);
+ }
+
+ @Test
+ public void testSortEqualsContent() {
+ Element emt = new Element("root");
+ final CDATA cdata = new CDATA("XXX");
+ final Text t1 = new Text("a");
+ final Text t2 = new Text("a");
+ final Text t3 = new Text("a");
+ final Text t4 = new Text("a");
+ emt.addContent(t1);
+ emt.addContent(cdata);
+ emt.addContent(t2);
+ emt.addContent(t3);
+ emt.addContent(t4);
+ assertEquals("aXXXaaa", emt.getText());
+
+ emt.sortContent(alphaContent);
+
+ assertEquals("aaaaXXX", emt.getText());
+ assertEquals(t1, emt.getContent(0));
+ assertEquals(t2, emt.getContent(1));
+ assertEquals(t3, emt.getContent(2));
+ assertEquals(t4, emt.getContent(3));
+ assertEquals(cdata, emt.getContent(4));
+ }
+
+ @Test
+ public void testSortInterleavedContent() {
+ Element emt = new Element("root");
+ emt.addContent(new Text("d"));
+ emt.addContent(new CDATA("ZZZ"));
+ emt.addContent(new Text("c"));
+ emt.addContent(new CDATA("YYY"));
+ emt.addContent(new Text("b"));
+ emt.addContent(new CDATA("XXX"));
+ emt.addContent(new Text("a"));
+ assertEquals("dZZZcYYYbXXXa", emt.getText());
+
+ // we can use Text comparator for CDATA too.
+ emt.sortContent(Filters.cdata(), alphaText);
+ assertEquals("dXXXcYYYbZZZa", emt.getText());
+
+ // we can use Text comparator for CDATA too.
+ emt.sortContent(new ContentFilter(ContentFilter.TEXT), alphaContent);
+ assertEquals("aXXXbYYYcZZZd", emt.getText());
+
+ // we can use Text comparator for CDATA too.... and Filters.text() does Text and CDATA
+ emt.sortContent(Filters.text(), alphaText);
+ assertEquals("XXXYYYZZZabcd", emt.getText());
+ }
+
+ @Test
+ public void testSortInterleavedEqualContent() {
+ Element emt = new Element("root");
+ final CDATA cd1 = new CDATA("ZZZ");
+ final CDATA cd2 = new CDATA("ZZZ");
+ final CDATA cd3 = new CDATA("ZZZ");
+ emt.addContent(new Text("d"));
+ emt.addContent(cd1);
+ emt.addContent(new Text("c"));
+ emt.addContent(cd2);
+ emt.addContent(new Text("b"));
+ emt.addContent(cd3);
+ emt.addContent(new Text("a"));
+ assertEquals("dZZZcZZZbZZZa", emt.getText());
+
+ assertTrue(emt.getContent(1) == cd1);
+ assertTrue(emt.getContent(3) == cd2);
+ assertTrue(emt.getContent(5) == cd3);
+
+ // we can use Text comparator for CDATA too.
+ // check sort does not reorder comp==0 content
+ emt.sortContent(Filters.cdata(), alphaText);
+ assertEquals("dZZZcZZZbZZZa", emt.getText());
+ assertTrue(emt.getContent(1) == cd1);
+ assertTrue(emt.getContent(3) == cd2);
+ assertTrue(emt.getContent(5) == cd3);
+
+
+ // we can use Text comparator for CDATA too.
+ emt.sortContent(new ContentFilter(ContentFilter.TEXT), alphaContent);
+ assertEquals("aZZZbZZZcZZZd", emt.getText());
+ assertTrue(emt.getContent(1) == cd1);
+ assertTrue(emt.getContent(3) == cd2);
+ assertTrue(emt.getContent(5) == cd3);
+
+ // we can use Text comparator for CDATA too.... and Filters.text() does Text and CDATA
+ emt.sortContent(Filters.text(), alphaText);
+ assertEquals("ZZZZZZZZZabcd", emt.getText());
+ assertTrue(emt.getContent(0) == cd1);
+ assertTrue(emt.getContent(1) == cd2);
+ assertTrue(emt.getContent(2) == cd3);
+ }
+
}

0 comments on commit 529a1f6

Please sign in to comment.
Something went wrong with that request. Please try again.