Reputation: 603
I want to iterate through a NodeList
using a for-each loop in Java. I have it working with a for loop and a do-while loop but not for-each.
NodeList nList = dom.getElementsByTagName("year");
do {
Element ele = (Element) nList.item(i);
list.add(ele.getElementsByTagName("MonthId").item(0).getTextContent());
i++;
} while (i < nList.getLength());
NodeList nList = dom.getElementsByTagName("year");
for (int i = 0; i < nList.getLength(); i++) {
Element ele = (Element) nList.item(i);
list.add(ele.getElementsByTagName("MonthId").item(0).getTextContent());
}
Upvotes: 60
Views: 84969
Reputation: 1098
It's not exactly an answer to the question but may still be helpful for others ending up here:
I also looked for a way to iterate over NodeList
but I usually prefer streams of loops. So I ended up with this method:
public static Stream<Node> streamElementsByTagName(Document dom, String tagName) {
var nodes = dom.getElementsByTagName(tagName);
return nodes == null
? Stream.empty()
: IntStream.range(0, nodes.getLength()).mapToObj(nodes::item);
}
Upvotes: 0
Reputation: 1009
I want to thank @Calin for the inspiration with the Kotlin code, but I want to go a little bit further and to be able to filter NodeList content by type and subclass in one line
fun <T : Node> NodeList.forEach(clazz : KClass<T>, vararg nodeType: Short, action: (T) -> Unit) {
(0 until this.length).asSequence().map { this.item(it) }
.filter { nodeType.isEmpty() || nodeType.contains(it.nodeType) }
.filter { clazz.isInstance(it) }.map { clazz.java.cast(it) }
.forEach { action(it) }
}
// original variant without any filtering, used for node's attributes
fun NamedNodeMap.forEach(action: (Node) -> Unit) {
(0 until this.length).asSequence().map { this.item(it) }
.forEach { action(it) }
}
Usage example:
xmlDoc.childNodes.forEach(Element::class, Node.ELEMENT_NODE) {
println("tag ${it.tagName} with attributes: ") // 'it' is an Element here
it.attributes.forEach { attr -> println("${attr.nodeName} - ${attr.nodeValue}")}
}
Upvotes: 0
Reputation: 139
One can use the Java8 stream to iterate the NodeList.
NodeList filterList = source.getChildNodes();
IntStream.range(0, filterList.getLength()).boxed().map(filterList::item).forEach(node -> {
});
Upvotes: 6
Reputation: 26180
There are ready to use or copypaste iterator implementations in org.apache.commons.collections4.iterators.NodeListIterator
and com.sun.xml.internal.ws.util.xml.NodeListIterator
.
Upvotes: 4
Reputation: 434
The validated solution is very useful, but here I share an improved solution based on the valid one, this helps you iterate as well, but easy to use, and secure:
public class XMLHelper {
private XMLHelper() { }
public static List<Node> getChildNodes(NodeList l) {
List<Node> children = Collections.<Node>emptyList();
if (l != null && l.getLength() > 0) {
if (l.item(0) != null && l.item(0).hasChildNodes()) {
children = new NodeListWrapper(l.item(0).getChildNodes());
}
}
return children;
}
public static List<Node> getChildNodes(Node n) {
List<Node> children = Collections.<Node>emptyList();
if (n != null && n.hasChildNodes()) {
NodeList l = n.getChildNodes();
if (l != null && l.getLength() > 0) {
children = new NodeListWrapper(l);
}
}
return children;
}
private static final class NodeListWrapper extends AbstractList<Node> implements RandomAccess {
private final NodeList list;
NodeListWrapper(NodeList l) {
list = l;
}
public Node get(int index) {
return list.item(index);
}
public int size() {
return list.getLength();
}
}
}
Usage:
for (Node inner : XMLHelper.getChildNodes(node)) { ... }
Thanks @Holger.
Upvotes: 1
Reputation: 10127
I know it is late to the party, but...
Since Java-8 you can write @RayHulha's solution even more concisely by using lambda expression (for creating a new Iterable
) and default method (for Iterator.remove
):
public static Iterable<Node> iterable(final NodeList nodeList) {
return () -> new Iterator<Node>() {
private int index = 0;
@Override
public boolean hasNext() {
return index < nodeList.getLength();
}
@Override
public Node next() {
if (!hasNext())
throw new NoSuchElementException();
return nodeList.item(index++);
}
};
}
and then use it like this:
NodeList nodeList = ...;
for (Node node : iterable(nodeList)) {
// ....
}
or equivalently like this:
NodeList nodeList = ...;
iterable(nodeList).forEach(node -> {
// ....
});
Upvotes: 23
Reputation: 6847
Adding the happy little kotlin version for sience:
fun NodeList.forEach(action: (Node) -> Unit) {
(0 until this.length)
.asSequence()
.map { this.item(it) }
.forEach { action(it) }
}
One can then use it with nodeList.forEach { do_something_awesome() }
Upvotes: 8
Reputation: 11
If the current DOM element is removed (via JavaScript) while iterating a NodeList (created from getElementsByTagName() and maybe others), the element will disappear from the NodeList. This makes correct iteration of the NodeList more tricky.
public class IteratableNodeList implements Iterable<Node> {
final NodeList nodeList;
public IteratableNodeList(final NodeList _nodeList) {
nodeList = _nodeList;
}
@Override
public Iterator<Node> iterator() {
return new Iterator<Node>() {
private int index = -1;
private Node lastNode = null;
private boolean isCurrentReplaced() {
return lastNode != null && index < nodeList.getLength() &&
lastNode != nodeList.item(index);
}
@Override
public boolean hasNext() {
return index + 1 < nodeList.getLength() || isCurrentReplaced();
}
@Override
public Node next() {
if (hasNext()) {
if (isCurrentReplaced()) {
// It got removed by a change in the DOM.
lastNode = nodeList.item(index);
} else {
lastNode = nodeList.item(++index);
}
return lastNode;
} else {
throw new NoSuchElementException();
}
}
@Override
public void remove() {
throw new UnsupportedOperationException();
}
};
}
public Stream<Node> stream() {
Spliterator<Node> spliterator =
Spliterators.spliterator(iterator(), nodeList.getLength(), 0);
return StreamSupport.stream(spliterator, false);
}
}
Then use it like this:
new IteratableNodeList(doc.getElementsByTagName(elementType)).
stream().filter(...)
Or:
new IteratableNodeList(doc.getElementsByTagName(elementType)).forEach(...)
Upvotes: 1
Reputation: 11221
public static Iterable<Node> iterable(final NodeList n) {
return new Iterable<Node>() {
@Override
public Iterator<Node> iterator() {
return new Iterator<Node>() {
int index = 0;
@Override
public boolean hasNext() {
return index < n.getLength();
}
@Override
public Node next() {
if (hasNext()) {
return n.item(index++);
} else {
throw new NoSuchElementException();
}
}
@Override
public void remove() {
throw new UnsupportedOperationException();
}
};
}
};
}
Upvotes: 12
Reputation: 298283
The workaround for this problem is straight-forward, and, thankfully you have to implements it only once.
import java.util.*;
import org.w3c.dom.*;
public final class XmlUtil {
private XmlUtil(){}
public static List<Node> asList(NodeList n) {
return n.getLength()==0?
Collections.<Node>emptyList(): new NodeListWrapper(n);
}
static final class NodeListWrapper extends AbstractList<Node>
implements RandomAccess {
private final NodeList list;
NodeListWrapper(NodeList l) {
list=l;
}
public Node get(int index) {
return list.item(index);
}
public int size() {
return list.getLength();
}
}
}
Once you have added this utility class to your project and added a static
import
for the XmlUtil.asList
method to your source code you can use it like this:
for(Node n: asList(dom.getElementsByTagName("year"))) {
…
}
Upvotes: 56
Reputation: 3543
As NodeList
is just an interface, you could create a class which would implement both NodeList
and Iterable
, in order to iterate through it.
Upvotes: 5
Reputation: 77187
NodeList
does not implement Iterable
, so you cannot use it with the enhanced for
loop.
Upvotes: 3