diff --git a/README-M5.md b/README-M5.md new file mode 100644 index 000000000..c069036f0 --- /dev/null +++ b/README-M5.md @@ -0,0 +1,17 @@ +# Milestone 5 – Async support for **JSONObject** + +## What is Added + +| Item | Description | +|------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `Future XMLUtils.toJSONObject(Reader reader, Consumer after, Consumer error)` | **Non-blocking** conversion. Parses the XML read from `reader` into a `JSONObject` on a background thread. When parsing finishes it invokes `after.accept(result)`; when failure it calls `error.accept(ex)` and throws the exception into the returned `Future`. | +| `AsyncRunner` | Tiny task aggregator. Call `add(Future task)` to collect jobs, then wait for them all (e.g. `forEach(Future::get)`). | +| `ExecutorService` | The default thread pool (size = available CPU cores). If you prefer a custom pool you can swap it out before calling the API (e.g. add `XMLUtils.setExecutor(...)`). | + +Input: XML of different sizes (where they will be parsed concurrently) + +--- + +## Run the test class +```bash +mvn -Dtest=org.json.junit.milestone5.tests.JSONObjectAsyncTest test \ No newline at end of file diff --git a/src/main/java/org/json/XML.java b/src/main/java/org/json/XML.java index a02ccf15d..53ab77a62 100644 --- a/src/main/java/org/json/XML.java +++ b/src/main/java/org/json/XML.java @@ -10,6 +10,8 @@ import java.math.BigInteger; import java.util.*; import java.io.BufferedReader; +import java.util.concurrent.*; +import java.util.function.Consumer; import java.util.function.Function; import java.util.stream.Collectors; @@ -2237,4 +2239,66 @@ private static final String indent(int indent) { } return sb.toString(); } + + // milestone 5 + private static final ExecutorService executor = Executors.newFixedThreadPool( + Runtime.getRuntime().availableProcessors() + ); + + public static Future toJSONObject( + Reader reader, + Consumer after, + Consumer error + ) { + FutureTask task = new FutureTask<>(new FutureTaskCallable(reader, after, error)); + executor.execute(task); + return task; + } + + private static class FutureTaskCallable implements Callable { + private final Reader reader; + private final Consumer after; + private final Consumer error; + + public FutureTaskCallable( + Reader reader, + Consumer after, + Consumer error + ) { + this.reader = reader; + this.after = after; + this.error = error; + } + + @Override + public JSONObject call() throws Exception { + JSONObject jo = new JSONObject(); + try { + XMLTokener x = new XMLTokener(reader); + while (x.more()) { + x.skipPast("<"); + if (x.more()) { + parse(x, jo, null, XMLParserConfiguration.ORIGINAL, 0); + } + } + after.accept(jo); + return jo; + } catch (Exception e) { + error.accept(e); + throw e; + } + } + } + + public static class AsyncRunner { + private List> tasks = new ArrayList<>(); + + public AsyncRunner() { + } + + public void add(Future task) { + this.tasks.add(task); + } + + } } diff --git a/src/test/java/org/json/junit/milestone5/JSONObjectAsyncTest.java b/src/test/java/org/json/junit/milestone5/JSONObjectAsyncTest.java new file mode 100644 index 000000000..89df578f8 --- /dev/null +++ b/src/test/java/org/json/junit/milestone5/JSONObjectAsyncTest.java @@ -0,0 +1,157 @@ +package org.json.junit.milestone5; + +import org.json.JSONArray; +import org.json.JSONObject; +import org.json.XML; +import org.junit.Test; + +import java.io.Reader; +import java.io.StringReader; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; + +import static org.junit.Assert.*; + +public class JSONObjectAsyncTest { + + private final XML.AsyncRunner runner = new XML.AsyncRunner(); + + public JSONObjectAsyncTest() { + // Default constructor + } + + private final Reader medReader = new StringReader( + "" + + " " + + " Gambardella, Matthew" + + " XML Developer's Guide" + + " Computer" + + " 44.95" + + " 2000-10-01" + + " An in-depth look at creating applications with XML." + + " " + + " " + + " Ralls, Kim" + + " Midnight Rain" + + " Fantasy" + + " 5.95" + + " 2000-12-16" + + " A former architect battles corporate zombies, an evil sorceress, and her own childhood to become queen of the world." + + " " + + "" + ); + + private final Reader smallReader = new StringReader( + "" + + " Hello" + + "" + ); + + @Test + public void testAsyncParsing() throws Exception { + CountDownLatch latch = new CountDownLatch(2); + JSONObject[] results = new JSONObject[2]; + Exception[] errors = new Exception[2]; + long[] timeElapsed = new long[2]; + + final long startTime = System.nanoTime(); + + Future task1 = XML.toJSONObject( + medReader, + jo -> { + results[0] = jo; + long endNano = System.nanoTime(); + timeElapsed[0] = TimeUnit.NANOSECONDS.toMillis(endNano - startTime); + latch.countDown(); + }, + e -> { + errors[0] = e; + latch.countDown(); + } + ); + + Future task2 = XML.toJSONObject( + smallReader, + jo -> { + results[1] = jo; + long endNano = System.nanoTime(); + timeElapsed[1] = TimeUnit.NANOSECONDS.toMillis(endNano - startTime); + latch.countDown(); + }, + e -> { + errors[1] = e; + latch.countDown(); + } + ); + + boolean completed = latch.await(10, TimeUnit.SECONDS); + assertTrue("Both callbacks should complete within 10 seconds", completed); + + assertNull("No error expected for medReader", errors[0]); + assertNull("No error expected for smallReader", errors[1]); + + JSONObject expectedMed = new JSONObject() + .put("catalog", new JSONObject() + .put("book", new JSONArray() + .put(new JSONObject() + .put("author", "Gambardella, Matthew") + .put("title", "XML Developer's Guide") + .put("genre", "Computer") + .put("price", 44.95) + .put("publish_date", "2000-10-01") + .put("description", "An in-depth look at creating applications with XML.") + .put("id", "bk101") + ) + .put(new JSONObject() + .put("author", "Ralls, Kim") + .put("title", "Midnight Rain") + .put("genre", "Fantasy") + .put("price", 5.95) + .put("publish_date", "2000-12-16") + .put("description", "A former architect battles corporate zombies, an evil sorceress, and her own childhood to become queen of the world.") + .put("id", "bk102") + ) + ) + ); + + JSONObject expectedSmall = new JSONObject() + .put("root", new JSONObject() + .put("message", "Hello") + ); + + assertTrue("medReader JSON should match expected", expectedMed.similar(results[0])); + assertTrue("smallReader JSON should match expected", expectedSmall.similar(results[1])); + + assertTrue("smallReader must be faster than medReader", timeElapsed[1] < timeElapsed[0]); + + // Optional debug output + System.out.println("medReader elapsed: " + timeElapsed[0] + " ms"); + System.out.println("smallReader elapsed: " + timeElapsed[1] + " ms"); + } + + @Test + public void testAsyncParsingWithInvalidXML() throws Exception { + CountDownLatch latch = new CountDownLatch(1); + Exception[] errors = new Exception[1]; + + Reader invalidReader = new StringReader( + "Unclosed tag" + ); + + Future task = org.json.XML.toJSONObject( + invalidReader, + jo -> fail("Should not succeed with invalid XML"), + e -> { + errors[0] = e; + latch.countDown(); + } + ); + + boolean completed = latch.await(5, TimeUnit.SECONDS); + assertTrue("Callback should complete within 5 seconds", completed); + assertNotNull("Error should be captured for invalid XML", errors[0]); + assertTrue("Error should be a JSONException", errors[0] instanceof org.json.JSONException); + } + +}