Repository: jhy/jsoup
Pull request #2246 (2024-12-15 23:49:46)

Removed previously deprecated class & method, enable Element Iterator
Removed UncheckedIOException (migrated to java.io.UncheckedIOException); signature change of Element#forEach to return void vs Element, to allow Element to implement Iterator.


Patch:
diff --git a/CHANGES.md b/CHANGES.md
index 854c7c12..2ff4d495 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -7,6 +7,9 @@
 * Updated the minimum Android API Level validation from 10 to **21**. As with previous jsoup versions, Android
   developers need to enable core library desugaring. The minimum Java version remains Java 8.
   [2173](https://github.com/jhy/jsoup/pull/2173)
+* Removed previously deprecated class: `org.jsoup.UncheckedIOException` (replace with `java.io.UncheckedIOException`);
+  method `Element Element#forEach(Consumer)` to
+  `void Element#forEach(Consumer())`. [2246](https://github.com/jhy/jsoup/pull/2246)
 
 ### Improvements
 
@@ -16,6 +19,7 @@
 * Added `Element#selectStream(String query)` and `Element#selectStream(Evaluator )` methods, that return a `Stream` of
   matching elements. Elements are evaluated and returned as they are found, and the stream can be
   terminated early. [2092](https://github.com/jhy/jsoup/pull/2092)
+* `Element` objects now implement `Iterable`, enabling them to be used in enhanced for loops.
 
 ### Bug Fixes
 
diff --git a/pom.xml b/pom.xml
index 859aab0d..ba818d38 100644
--- a/pom.xml
+++ b/pom.xml
@@ -260,7 +260,6 @@
             <breakBuildOnSourceIncompatibleModifications>true</breakBuildOnSourceIncompatibleModifications>
             <excludes>
               <exclude>@java.lang.Deprecated</exclude>
-              <exclude>org.jsoup.UncheckedIOException</exclude>
             </excludes>
             <overrideCompatibilityChangeParameters>
               <!-- allows new default and move to default methods. compatible as long as existing binaries aren't making calls via reflection. if so, they need to catch errors anyway. -->
diff --git a/src/main/java/org/jsoup/UncheckedIOException.java b/src/main/java/org/jsoup/UncheckedIOException.java
deleted file mode 100644
index a3b4fa31..00000000
--- a/src/main/java/org/jsoup/UncheckedIOException.java
+++ /dev/null
@@ -1,22 +0,0 @@
-package org.jsoup;
-
-import java.io.IOException;
-
-/**
- * @deprecated Use {@link java.io.UncheckedIOException} instead. This class acted as a compatibility shim for Java
- * versions prior to 1.8.
- */
-@Deprecated
-public class UncheckedIOException extends java.io.UncheckedIOException {
-    public UncheckedIOException(IOException cause) {
-        super(cause);
-    }
-
-    public UncheckedIOException(String message) {
-        super(new IOException(message));
-    }
-
-    public IOException ioException() {
-        return getCause();
-    }
-}
diff --git a/src/main/java/org/jsoup/helper/HttpConnection.java b/src/main/java/org/jsoup/helper/HttpConnection.java
index a33bcd91..501cce58 100644
--- a/src/main/java/org/jsoup/helper/HttpConnection.java
+++ b/src/main/java/org/jsoup/helper/HttpConnection.java
@@ -3,7 +3,6 @@ package org.jsoup.helper;
 import org.jsoup.Connection;
 import org.jsoup.HttpStatusException;
 import org.jsoup.Progress;
-import org.jsoup.UncheckedIOException;
 import org.jsoup.UnsupportedMimeTypeException;
 import org.jsoup.internal.ControllableInputStream;
 import org.jsoup.internal.Functions;
@@ -11,7 +10,6 @@ import org.jsoup.internal.StringUtil;
 import org.jsoup.nodes.Document;
 import org.jsoup.parser.Parser;
 import org.jsoup.parser.StreamParser;
-import org.jsoup.parser.TokenQueue;
 import org.jspecify.annotations.Nullable;
 
 import javax.net.ssl.HttpsURLConnection;
@@ -25,6 +23,7 @@ import java.io.InputStream;
 import java.io.InputStreamReader;
 import java.io.OutputStream;
 import java.io.OutputStreamWriter;
+import java.io.UncheckedIOException;
 import java.net.CookieManager;
 import java.net.CookieStore;
 import java.net.HttpURLConnection;
diff --git a/src/main/java/org/jsoup/nodes/Element.java b/src/main/java/org/jsoup/nodes/Element.java
index 1a1538ed..1536cda4 100644
--- a/src/main/java/org/jsoup/nodes/Element.java
+++ b/src/main/java/org/jsoup/nodes/Element.java
@@ -23,6 +23,7 @@ import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
+import java.util.Iterator;
 import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Map;
@@ -44,7 +45,7 @@ import static org.jsoup.parser.TokenQueue.escapeCssIdentifier;
  <p>
  From an Element, you can extract data, traverse the node graph, and manipulate the HTML.
 */
-public class Element extends Node {
+public class Element extends Node implements Iterable<Element> {
     private static final List<Element> EmptyChildren = Collections.emptyList();
     private static final Pattern ClassSplit = Pattern.compile("\\s+");
     private static final String BaseUriKey = Attributes.internalKey("baseUri");
@@ -1911,15 +1912,20 @@ public class Element extends Node {
      Perform the supplied action on this Element and each of its descendant Elements, during a depth-first traversal.
      Elements may be inspected, changed, added, replaced, or removed.
      @param action the function to perform on the element
-     @return this Element, for chaining
      @see Node#forEachNode(Consumer)
-     @deprecated use {@link #stream()}.{@link Stream#forEach(Consumer) forEach(Consumer)} instead. (Removing this method
-     so Element can implement Iterable, which this signature conflicts with due to the non-void return.)
      */
-    @Deprecated
-    public Element forEach(Consumer<? super Element> action) {
+    @Override
+    public void forEach(Consumer<? super Element> action) {
         stream().forEach(action);
-        return this;
+    }
+
+    /**
+     Returns an Iterator that iterates this Element and each of its descendant Elements, in document order.
+     @return an Iterator
+     */
+    @Override
+    public Iterator<Element> iterator() {
+        return new NodeIterator<>(this, Element.class);
     }
 
     @Override
diff --git a/src/main/java/org/jsoup/parser/CharacterReader.java b/src/main/java/org/jsoup/parser/CharacterReader.java
index 9c16d44d..80359393 100644
--- a/src/main/java/org/jsoup/parser/CharacterReader.java
+++ b/src/main/java/org/jsoup/parser/CharacterReader.java
@@ -1,11 +1,11 @@
 package org.jsoup.parser;
 
-import org.jsoup.UncheckedIOException;
 import org.jsoup.helper.Validate;
 import org.jsoup.internal.SoftPool;
 import org.jspecify.annotations.Nullable;
 
 import java.io.IOException;
+import java.io.UncheckedIOException;
 import java.io.Reader;
 import java.io.StringReader;
 import java.util.ArrayList;


Test patch:
diff --git a/src/test/java/org/jsoup/nodes/ElementTest.java b/src/test/java/org/jsoup/nodes/ElementTest.java
index 2c2fd50d..b53444ae 100644
--- a/src/test/java/org/jsoup/nodes/ElementTest.java
+++ b/src/test/java/org/jsoup/nodes/ElementTest.java
@@ -18,6 +18,7 @@ import org.junit.jupiter.params.provider.MethodSource;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.HashSet;
+import java.util.Iterator;
 import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Map;
@@ -26,6 +27,8 @@ import java.util.concurrent.atomic.AtomicInteger;
 import java.util.regex.Pattern;
 import java.util.stream.Stream;
 
+import static org.jsoup.nodes.NodeIteratorTest.assertIterates;
+import static org.jsoup.nodes.NodeIteratorTest.trackSeen;
 import static org.jsoup.select.SelectorTest.assertSelectedOwnText;
 import static org.junit.jupiter.api.Assertions.*;
 
@@ -2992,4 +2995,22 @@ public class ElementTest {
 
         assertEquals("Hello world", div.text());
     }
+
+    @Test void elementIsIterable() {
+        Document doc = Jsoup.parse("<div><a id=1>One</a> Two <a id=2>Three<b>Four</a><a id=3>Five</a></div>");
+        String expect = "div;a#1;a#2;b;b;a#3;"; // elements only, in doc order
+        Element div = doc.expectFirst("div");
+
+        // for each pattern
+        StringBuilder seen = new StringBuilder();
+        for (Element el: div) {
+            trackSeen(el, seen);
+        }
+        assertEquals(expect, seen.toString());
+
+        // iterator
+        seen = new StringBuilder();
+        Iterator<Element> iterator = div.iterator();
+        assertIterates(iterator, expect);
+    }
 }
diff --git a/src/test/java/org/jsoup/nodes/NodeIteratorTest.java b/src/test/java/org/jsoup/nodes/NodeIteratorTest.java
index ab7e9345..b5b77fbc 100644
--- a/src/test/java/org/jsoup/nodes/NodeIteratorTest.java
+++ b/src/test/java/org/jsoup/nodes/NodeIteratorTest.java
@@ -3,6 +3,7 @@ package org.jsoup.nodes;
 import org.jsoup.Jsoup;
 import org.junit.jupiter.api.Test;
 
+import java.util.Iterator;
 import java.util.NoSuchElementException;
 
 import static org.junit.jupiter.api.Assertions.*;
@@ -230,7 +231,7 @@ class NodeIteratorTest {
         assertContents(doc, "#root;html;head;body;div#1;p;One++;p;Two++;div#2;p;Three++;p;Four++;");
     }
 
-    static <T extends Node> void assertIterates(NodeIterator<T> it, String expected) {
+    static <T extends Node> void assertIterates(Iterator<T> it, String expected) {
         Node previous = null;
         StringBuilder actual = new StringBuilder();
         while (it.hasNext()) {
@@ -263,4 +264,4 @@ class NodeIteratorTest {
         actual.append(";");
     }
 
-}
\ No newline at end of file
+}