Inspired by the answer of Andrey Lebedenko.
Capable of sorting by a Nodes attribute or by a Nodes text content.
Ready to be used in Your XML utility class.
public static Collection<Node> nodeListCollection(final NodeList nodeList) {
if (nodeList == null) {
return Collections.emptyList();
}
final int length = nodeList.getLength();
if (length == 0) {
return Collections.emptyList();
}
return IntStream.range(0, length)
.mapToObj(nodeList::item)
.collect(Collectors.toList());
}
private static int compareString(final String str1, final String str2, final boolean nullIsLess) {
if (Objects.equals(str1, str2)) {
return 0;
}
if (str1 == null) {
return nullIsLess ? -1 : 1;
}
if (str2 == null) {
return nullIsLess ? 1 : -1;
}
return str1.compareTo(str2);
}
private static final Function<Boolean, Comparator<Node>> StringNodeValueComparatorSupplier = (asc) ->
(Node a, Node b) -> {
final String va = a == null ? null : a.getTextContent();
final String vb = b == null ? null : b.getTextContent();
return (asc ? 1 : -1) * compareString(va, vb,asc);
};
private static final BiFunction<Boolean, String, Comparator<Node>> StringNodeAttributeComparatorSupplier = (asc, attrName) ->
(Node a, Node b) -> {
final String va = a == null ? null : a.hasAttributes() ?
((Element) a).getAttribute(attrName) : null;
final String vb = b == null ? null : b.hasAttributes() ?
((Element) b).getAttribute(attrName) : null;
return (asc ? 1 : -1) * compareString(va, vb,asc);
};
private static <T extends Comparable<T>> Comparator<Node> nodeComparator(
final boolean asc,
final boolean useAttr,
final String attribute,
final Constructor<T> constructor
) {
return (Node a, Node b) -> {
if (a == null && b == null) {
return 0;
} else if (a == null) {
return (asc ? -1 : 1);
} else if (b == null) {
return (asc ? 1 : -1);
}
T aV;
try {
final String aStr;
if (useAttr) {
aStr = a.hasAttributes() ? ((Element) a).getAttribute(attribute) : null;
} else {
aStr = a.getTextContent();
}
aV = aStr == null || aStr.matches("\\s+") ? null : constructor.newInstance(aStr);
} catch (Exception ignored) {
aV = null;
}
T bV;
try {
final String bStr;
if (useAttr) {
bStr = b.hasAttributes() ? ((Element) b).getAttribute(attribute) : null;
} else {
bStr = b.getTextContent();
}
bV = bStr == null || bStr.matches("\\s+") ? null : constructor.newInstance(bStr);
} catch (Exception ignored) {
bV = null;
}
final int ret;
if (aV == null && bV == null) {
ret = 0;
} else if (aV == null) {
ret = -1;
} else if (bV == null) {
ret = 1;
} else {
ret = aV.compareTo(bV);
}
return (asc ? 1 : -1) * ret;
};
}
/**
* Method to sort any NodeList by an attribute all nodes must have. <br>If the attribute is absent for a signle
* {@link Node} or the {@link NodeList} does contain elements without Attributes, null is used instead. <br>If
* <code>asc</code> is
* <code>true</code>, nulls first, else nulls last.
*
* @param nodeList The {@link NodeList} containing all {@link Node} to sort.
* @param attribute Name of the attribute to extract and compare
* @param asc <code>true</code>: ascending, <code>false</code>: descending
* @param compareType Optional class to use for comparison. Must implement {@link Comparable} and have Constructor
* that takes a single {@link String} argument. If <code>null</code> is supplied, {@link String} is used.
* @return A collection of the {@link Node}s passed as {@link NodeList}
* @throws RuntimeException If <code>compareType</code> does not have a constructor taking a single {@link String}
* argument. Also, if the comparator created does violate the {@link Comparator} contract, an
* {@link IllegalArgumentException} is raised.
* @implNote Exceptions during calls of the single String argument constructor of <code>compareType</code> are
* ignored. Values are substituted by <code>null</code>
*/
public static <T extends Comparable<T>> Collection<Node> sortNodesByAttribute(
final NodeList nodeList,
String attribute,
boolean asc,
Class<T> compareType) {
final Comparator<Node> nodeComparator;
if (compareType == null) {
nodeComparator = StringNodeAttributeComparatorSupplier.apply(asc, attribute);
} else {
final Constructor<T> constructor;
try {
constructor = compareType.getDeclaredConstructor(String.class);
} catch (NoSuchMethodException e) {
throw new RuntimeException(
"Cannot compare Node Attribute '" + attribute + "' using the Type '" + compareType.getName()
+ "': No Constructor available that takes a single String argument.", e);
}
nodeComparator = nodeComparator(asc, true, attribute, constructor);
}
final List<Node> nodes = new ArrayList<>(nodeListCollection(nodeList));
nodes.sort(nodeComparator);
return nodes;
}
/**
* Method to sort any NodeList by their text content using an optional type. <br>If
* <code>asc</code> is
* <code>true</code>, nulls first, else nulls last.
*
* @param nodeList The {@link NodeList} containing all {@link Node}s to sort.
* @param asc <code>true</code>: ascending, <code>false</code>: descending
* @param compareType Optional class to use for comparison. Must implement {@link Comparable} and have Constructor
* that takes a single {@link String} argument. If <code>null</code> is supplied, {@link String} is used.
* @return A collection of the {@link Node}s passed as {@link NodeList}
* @throws RuntimeException If <code>compareType</code> does not have a constructor taking a single {@link String}
* argument. Also, if the comparator created does violate the {@link Comparator} contract, an
* {@link IllegalArgumentException} is raised.
* @implNote Exceptions during calls of the single String argument constructor of <code>compareType</code> are
* ignored. Values are substituted by <code>null</code>
*/
public static <T extends Comparable<T>> Collection<Node> sortNodes(
final NodeList nodeList,
boolean asc,
Class<T> compareType) {
final Comparator<Node> nodeComparator;
if (compareType == null) {
nodeComparator = StringNodeValueComparatorSupplier.apply(asc);
} else {
final Constructor<T> constructor;
try {
constructor = compareType.getDeclaredConstructor(String.class);
} catch (NoSuchMethodException e) {
throw new RuntimeException(
"Cannot compare Nodes using the Type '" + compareType.getName()
+ "': No Constructor available that takes a single String argument.", e);
}
nodeComparator = nodeComparator(asc, false, null, constructor);
}
final List<Node> nodes = new ArrayList<>(nodeListCollection(nodeList));
nodes.sort(nodeComparator);
return nodes;
}