View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *      http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.apache.juneau;
18  
19  import static java.util.stream.Collectors.*;
20  import static org.junit.jupiter.api.Assertions.*;
21  
22  import java.io.*;
23  import java.lang.reflect.*;
24  import java.net.*;
25  import java.util.*;
26  import java.util.function.*;
27  import java.util.regex.*;
28  import java.util.stream.*;
29  
30  import org.apache.juneau.annotation.*;
31  import org.apache.juneau.bean.swagger.*;
32  import org.apache.juneau.common.utils.Utils;
33  import org.apache.juneau.internal.*;
34  import org.apache.juneau.junit.bct.*;
35  import org.apache.juneau.marshaller.*;
36  import org.apache.juneau.rest.*;
37  import org.apache.juneau.rest.mock.*;
38  import org.apache.juneau.serializer.*;
39  import org.apache.juneau.xml.*;
40  import org.junit.jupiter.api.*;
41  
42  /**
43   * Comprehensive utility class for Bean-Centric Tests (BCT) and general testing operations.
44   *
45   * <p>This class provides the core testing infrastructure for Apache Juneau, with particular emphasis
46   * on the Bean-Centric Testing (BCT) framework. BCT enables sophisticated assertion patterns for
47   * testing object properties, collections, maps, and complex nested structures with minimal code.</p>
48   *
49   * <h5 class='section'>Bean-Centric Testing (BCT) Framework:</h5>
50   * <p>The BCT framework consists of several key components:</p>
51   * <ul>
52   * 	<li><b>{@link BeanConverter}:</b> Core interface for object conversion and property access</li>
53   * 	<li><b>{@link BasicBeanConverter}:</b> Default implementation with extensible type handlers</li>
54   * 	<li><b>Assertion Methods:</b> High-level testing methods that leverage the converter framework</li>
55   * </ul>
56   *
57   * <h5 class='section'>Primary BCT Assertion Methods:</h5>
58   * <dl>
59   * 	<dt><b>{@link #assertBean(Object, String, String)}</b></dt>
60   * 	<dd>Tests object properties with nested syntax support and collection iteration</dd>
61   *
62   * 	<dt><b>{@link #assertMap(Map, String, String)}</b></dt>
63   * 	<dd>Tests map entries with the same nested property syntax as assertBean</dd>
64   *
65   * 	<dt><b>{@link #assertMapped(Object, java.util.function.BiFunction, String, String)}</b></dt>
66   * 	<dd>Tests custom property access using BiFunction for non-standard objects</dd>
67   *
68   * 	<dt><b>{@link #assertList(List, Object...)}</b></dt>
69   * 	<dd>Tests list/collection elements with varargs for expected values</dd>
70   *
71   * 	<dt><b>{@link #assertBeans(Collection, String, String...)}</b></dt>
72   * 	<dd>Tests collections of objects by extracting and comparing specific fields</dd>
73   * </dl>
74   *
75   * <h5 class='section'>BCT Advanced Features:</h5>
76   * <ul>
77   * 	<li><b>Nested Property Syntax:</b> "address{street,city}" for testing nested objects</li>
78   * 	<li><b>Collection Iteration:</b> "#{property}" syntax for testing all elements</li>
79   * 	<li><b>Universal Size Properties:</b> "length" and "size" work on all collection types</li>
80   * 	<li><b>Array/List Access:</b> Numeric indices for element-specific testing</li>
81   * 	<li><b>Method Chaining:</b> Fluent setters can be tested directly</li>
82   * 	<li><b>Direct Field Access:</b> Public fields accessed without getters</li>
83   * 	<li><b>Map Key Access:</b> Including special "&lt;NULL&gt;" syntax for null keys</li>
84   * </ul>
85   *
86   * <h5 class='section'>Converter Extensibility:</h5>
87   * <p>The BCT framework is built on the extensible {@link BasicBeanConverter} which allows:</p>
88   * <ul>
89   * 	<li><b>Custom Stringifiers:</b> Type-specific string conversion logic</li>
90   * 	<li><b>Custom Listifiers:</b> Collection-type conversion for iteration</li>
91   * 	<li><b>Custom Swapifiers:</b> Object transformation before conversion</li>
92   * 	<li><b>Configurable Settings:</b> Formatting, delimiters, and display options</li>
93   * </ul>
94   *
95   * <h5 class='section'>Usage Examples:</h5>
96   *
97   * <p><b>Basic Property Testing:</b></p>
98   * <p class='bjava'>
99   * 	<jc>// Test multiple properties</jc>
100  * 	assertBean(user, <js>"name,age,active"</js>, <js>"John,30,true"</js>);
101  *
102  * 	<jc>// Test nested properties</jc>
103  * 	assertBean(user, <js>"address{street,city}"</js>, <js>"{123 Main St,Springfield}"</js>);
104  * </p>
105  *
106  * <p><b>Collection and Array Testing:</b></p>
107  * <p class='bjava'>
108  * 	<jc>// Test collection size and iterate over all elements</jc>
109  * 	assertBean(order, <js>"items{length,#{name}}"</js>, <js>"{3,[{Laptop},{Phone},{Tablet}]}"</js>);
110  *
111  * 	<jc>// Test specific array elements</jc>
112  * 	assertBean(data, <js>"values{0,1,2}"</js>, <js>"{100,200,300}"</js>);
113  * </p>
114  *
115  * <p><b>Map and Collection Testing:</b></p>
116  * <p class='bjava'>
117  * 	<jc>// Test map entries</jc>
118  * 	assertMap(config, <js>"timeout,retries"</js>, <js>"30000,3"</js>);
119  *
120  * 	<jc>// Test list elements</jc>
121  * 	assertList(tags, <js>"red"</js>, <js>"green"</js>, <js>"blue"</js>);
122  * </p>
123  *
124  * <p><b>Custom Property Access:</b></p>
125  * <p class='bjava'>
126  * 	<jc>// Test with custom accessor function</jc>
127  * 	assertMapped(myObject, (obj, prop) -> obj.getProperty(prop),
128  * 		<js>"prop1,prop2"</js>, <js>"value1,value2"</js>);
129  * </p>
130  *
131  * <h5 class='section'>Performance and Thread Safety:</h5>
132  * <p>The BCT framework is designed for high performance with:</p>
133  * <ul>
134  * 	<li><b>Caching:</b> Type-to-handler mappings cached for fast lookup</li>
135  * 	<li><b>Thread Safety:</b> All operations are thread-safe for concurrent testing</li>
136  * 	<li><b>Minimal Allocation:</b> Efficient object reuse and minimal temporary objects</li>
137  * </ul>
138  *
139  * @see BeanConverter
140  * @see BasicBeanConverter
141  */
142 public class TestUtils extends Utils2 {
143 
144 	private static final ThreadLocal<TimeZone> SYSTEM_TIME_ZONE = new ThreadLocal<>();
145 
146 	public static final ThreadLocal<Locale> SYSTEM_LOCALE = new ThreadLocal<>();
147 
148 	/**
149 	 * Asserts that the fields/properties on the specified bean are the specified values after being converted to {@link Utils#r readable} strings.
150 	 *
151 	 * <p>This is the primary method for Bean-Centric Tests (BCT), supporting extensive property validation
152 	 * patterns including nested objects, collections, arrays, method chaining, direct field access, collection iteration
153 	 * with <js>#{property}</js> syntax, and universal <js>length</js>/<js>size</js> properties for all collection types.</p>
154 	 *
155 	 * <p>The method uses the {@link BasicBeanConverter#DEFAULT} converter internally for object introspection
156 	 * and value extraction. The converter provides sophisticated property access through the {@link BeanConverter}
157 	 * interface, supporting multiple fallback mechanisms for accessing object properties and values.</p>
158 	 *
159 	 * <h5 class='section'>Basic Usage:</h5>
160 	 * <p class='bjava'>
161 	 * 	<jc>// Test multiple properties</jc>
162 	 * 	assertBean(myBean, <js>"prop1,prop2,prop3"</js>, <js>"val1,val2,val3"</js>);
163 	 *
164 	 * 	<jc>// Test single property</jc>
165 	 * 	assertBean(myBean, <js>"name"</js>, <js>"John"</js>);
166 	 * </p>
167 	 *
168 	 * <h5 class='section'>Nested Property Testing:</h5>
169 	 * <p class='bjava'>
170 	 * 	<jc>// Test nested bean properties</jc>
171 	 * 	assertBean(myBean, <js>"address{street,city,state}"</js>, <js>"{123 Main St,Springfield,IL}"</js>);
172 	 *
173 	 * 	<jc>// Test arbitrarily deep nesting</jc>
174 	 * 	assertBean(myBean, <js>"person{address{geo{lat,lon}}}"</js>, <js>"{{{{40.7,-74.0}}}}"</js>);
175 	 * </p>
176 	 *
177 	  * <h5 class='section'>Array, List, and Stream Testing:</h5>
178 	 * <p class='bjava'>
179 	 * 	<jc>// Test array/list elements by index</jc>
180 	 * 	assertBean(myBean, <js>"items{0,1,2}"</js>, <js>"{item1,item2,item3}"</js>);
181 	 *
182 	 * 	<jc>// Test nested properties within array elements</jc>
183 	 * 	assertBean(myBean, <js>"orders{0{id,total}}"</js>, <js>"{{123,99.95}}"</js>);
184 	 *
185 	 * 	<jc>// Test array length property</jc>
186 	 * 	assertBean(myBean, <js>"items{length}"</js>, <js>"{5}"</js>);
187 	 *
188 	 * 	<jc>// Works with any iterable type including Streams</jc>
189 	 * 	assertBean(myBean, <js>"userStream{#{name}}"</js>, <js>"[{Alice},{Bob}]"</js>);
190 	 * </p>
191 	 *
192 	 * <h5 class='section'>Collection Iteration Syntax:</h5>
193 	 * <p class='bjava'>
194 	 * 	<jc>// Test properties across ALL elements in a collection using #{...} syntax</jc>
195 	 * 	assertBean(myBean, <js>"userList{#{name}}"</js>, <js>"[{John},{Jane},{Bob}]"</js>);
196 	 *
197 	 * 	<jc>// Test multiple properties from each element</jc>
198 	 * 	assertBean(myBean, <js>"orderList{#{id,status}}"</js>, <js>"[{123,ACTIVE},{124,PENDING}]"</js>);
199 	 *
200 	 * 	<jc>// Works with nested properties within each element</jc>
201 	 * 	assertBean(myBean, <js>"customers{#{address{city}}}"</js>, <js>"[{{New York}},{{Los Angeles}}]"</js>);
202 	 *
203 	  * 	<jc>// Works with arrays and any iterable collection type (including Streams)</jc>
204 	 * 	assertBean(config, <js>"itemArray{#{type}}"</js>, <js>"[{String},{Integer},{Boolean}]"</js>);
205 	 * 	assertBean(data, <js>"statusSet{#{name}}"</js>, <js>"[{ACTIVE},{PENDING},{CANCELLED}]"</js>);
206 	 * 	assertBean(processor, <js>"dataStream{#{value}}"</js>, <js>"[{A},{B},{C}]"</js>);
207 	 * </p>
208 	 *
209 	 * <h5 class='section'>Universal Collection Size Properties:</h5>
210 	 * <p class='bjava'>
211 	 * 	<jc>// Both 'length' and 'size' work universally across all collection types</jc>
212 	 * 	assertBean(myBean, <js>"myArray{length}"</js>, <js>"{5}"</js>);        <jc>// Arrays</jc>
213 	 * 	assertBean(myBean, <js>"myArray{size}"</js>, <js>"{5}"</js>);          <jc>// Also works for arrays</jc>
214 	 *
215 	 * 	assertBean(myBean, <js>"myList{size}"</js>, <js>"{3}"</js>);           <jc>// Collections</jc>
216 	 * 	assertBean(myBean, <js>"myList{length}"</js>, <js>"{3}"</js>);         <jc>// Also works for collections</jc>
217 	 *
218 	 * 	assertBean(myBean, <js>"myMap{size}"</js>, <js>"{7}"</js>);            <jc>// Maps</jc>
219 	 * 	assertBean(myBean, <js>"myMap{length}"</js>, <js>"{7}"</js>);          <jc>// Also works for maps</jc>
220 	 * </p>
221 	 *
222 	 * <h5 class='section'>Class Name Testing:</h5>
223 	 * <p class='bjava'>
224 	 * 	<jc>// Test class properties (prefer simple names for maintainability)</jc>
225 	 * 	assertBean(myBean, <js>"obj{class{simpleName}}"</js>, <js>"{{MyClass}}"</js>);
226 	 *
227 	 * 	<jc>// Test full class names when needed</jc>
228 	 * 	assertBean(myBean, <js>"obj{class{name}}"</js>, <js>"{{com.example.MyClass}}"</js>);
229 	 * </p>
230 	 *
231 	 * <h5 class='section'>Method Chaining Support:</h5>
232 	 * <p class='bjava'>
233 	 * 	<jc>// Test fluent setter chains (returns same object)</jc>
234 	 * 	assertBean(
235 	 * 		item.setType(<js>"foo"</js>).setFormat(<js>"bar"</js>).setDefault(<js>"baz"</js>),
236 	 * 		<js>"type,format,default"</js>,
237 	 * 		<js>"foo,bar,baz"</js>
238 	 * 	);
239 	 * </p>
240 	 *
241 	 * <h5 class='section'>Advanced Collection Analysis:</h5>
242 	 * <p class='bjava'>
243 	 * 	<jc>// Combine size/length, metadata, and content iteration in single assertions</jc>
244 	 * 	assertBean(myBean, <js>"users{length,class{simpleName},#{name}}"</js>,
245 	 * 		<js>"{3,{ArrayList},[{John},{Jane},{Bob}]}"</js>);
246 	 *
247 	 * 	<jc>// Comprehensive collection validation with multiple iteration patterns</jc>
248 	 * 	assertBean(order, <js>"items{size,#{name},#{price}}"</js>,
249 	 * 		<js>"{3,[{Laptop},{Phone},{Tablet}],[{999.99},{599.99},{399.99}]}"</js>);
250 	 *
251 	 * 	<jc>// Perfect for validation testing - verify error count and details</jc>
252 	 * 	assertBean(result, <js>"errors{length,#{field},#{code}}"</js>,
253 	 * 		<js>"{2,[{email},{password}],[{E001},{E002}]}"</js>);
254 	 *
255 	 * 	<jc>// Mixed collection types with consistent syntax</jc>
256 	 * 	assertBean(response, <js>"results{size},metadata{length}"</js>, <js>"{25},{4}"</js>);
257 	 * </p>
258 	 *
259 	 * <h5 class='section'>Direct Field Access:</h5>
260 	 * <p class='bjava'>
261 	 * 	<jc>// Test public fields directly (no getters required)</jc>
262 	 * 	assertBean(myBean, <js>"f1,f2,f3"</js>, <js>"val1,val2,val3"</js>);
263 	 *
264 	 * 	<jc>// Test field properties with chaining</jc>
265 	 * 	assertBean(myBean, <js>"f1{length},f2{class{simpleName}}"</js>, <js>"{5},{{String}}"</js>);
266 	 * </p>
267 	 *
268 	  * <h5 class='section'>Map Testing:</h5>
269 	 * <p class='bjava'>
270 	 * 	<jc>// Test map values by key</jc>
271 	 * 	assertBean(myBean, <js>"configMap{timeout,retries}"</js>, <js>"{30000,3}"</js>);
272 	 *
273 	 * 	<jc>// Test map size</jc>
274 	 * 	assertBean(myBean, <js>"settings{size}"</js>, <js>"{5}"</js>);
275 	 *
276 	 * 	<jc>// Test null keys using special &lt;NULL&gt; syntax</jc>
277 	 * 	assertBean(myBean, <js>"mapWithNullKey{&lt;NULL&gt;}"</js>, <js>"{nullKeyValue}"</js>);
278 	 * </p>
279 	 *
280 	 * <h5 class='section'>Collection and Boolean Values:</h5>
281 	 * <p class='bjava'>
282 	 * 	<jc>// Test boolean values</jc>
283 	 * 	assertBean(myBean, <js>"enabled,visible"</js>, <js>"true,false"</js>);
284 	 *
285 	 * 	<jc>// Test enum collections</jc>
286 	 * 	assertBean(myBean, <js>"statuses"</js>, <js>"[ACTIVE,PENDING]"</js>);
287 	 * </p>
288 	 *
289 	 * <h5 class='section'>Value Syntax Rules:</h5>
290 	 * <ul>
291 	 * 	<li><b>Simple values:</b> <js>"value"</js> for direct property values</li>
292 	 * 	<li><b>Nested values:</b> <js>"{value}"</js> for single-level nested properties</li>
293 	 * 	<li><b>Deep nested values:</b> <js>"{{value}}"</js>, <js>"{{{value}}}"</js> for multiple nesting levels</li>
294 	 * 	<li><b>Array/Collection values:</b> <js>"[item1,item2]"</js> for collections</li>
295 	 * 	<li><b>Collection iteration:</b> <js>"#{property}"</js> iterates over ALL collection elements, returns <js>"[{val1},{val2}]"</js></li>
296 	 * 	<li><b>Universal size properties:</b> <js>"length"</js> and <js>"size"</js> work on arrays, collections, and maps</li>
297 	 * 	<li><b>Boolean values:</b> <js>"true"</js>, <js>"false"</js></li>
298 	 * 	<li><b>Null values:</b> <js>"null"</js></li>
299 	 * </ul>
300 	 *
301 	  * <h5 class='section'>Property Access Priority:</h5>
302 	 * <ol>
303 	 * 	<li><b>Collection/Array access:</b> Numeric indices for arrays/lists (e.g., <js>"0"</js>, <js>"1"</js>)</li>
304 	 * 	<li><b>Universal size properties:</b> <js>"length"</js> and <js>"size"</js> for arrays, collections, and maps</li>
305 	 * 	<li><b>Map key access:</b> Direct key lookup for Map objects (including <js>"&lt;NULL&gt;"</js> for null keys)</li>
306 	 * 	<li><b>is{Property}()</b> methods (for boolean properties)</li>
307 	 * 	<li><b>get{Property}()</b> methods</li>
308 	 * 	<li><b>Public fields</b> (direct field access)</li>
309 	 * </ol>
310 	 *
311 	 * @param actual The bean object to test. Must not be null.
312 	 * @param fields Comma-delimited list of property names to test. Supports nested syntax with {}.
313 	 * @param expected Comma-delimited list of expected values. Must match the order of fields.
314 	 * @throws NullPointerException if the bean is null
315 	 * @throws AssertionError if any property values don't match expected values
316 	 * @see BeanConverter
317 	 * @see BasicBeanConverter
318 	 */
319 	public static void assertBean(Object actual, String fields, String expected) {
320 		BctAssertions.assertBean(actual, fields, expected);
321 	}
322 
323 	/**
324 	 * Asserts that multiple beans in a collection have the expected property values.
325 	 *
326 	 * <p>This method validates that each bean in a collection has the specified property values,
327 	 * using the same property access logic as {@link #assertBean(Object, String, String)}.
328 	 * It's perfect for testing collections of similar objects or validation results.</p>
329 	 *
330 	 * <h5 class='section'>Basic Usage:</h5>
331 	 * <p class='bjava'>
332 	 * 	<jc>// Test list of user beans</jc>
333 	 * 	assertBeans(userList, <js>"name,age"</js>,
334 	 * 		<js>"John,25"</js>, <js>"Jane,30"</js>, <js>"Bob,35"</js>);
335 	 * </p>
336 	 *
337 	 * <h5 class='section'>Complex Property Testing:</h5>
338 	 * <p class='bjava'>
339 	 * 	<jc>// Test nested properties across multiple beans</jc>
340 	 * 	assertBeans(orderList, <js>"id,customer{name,email}"</js>,
341 	 * 		<js>"1,{John,john@example.com}"</js>,
342 	 * 		<js>"2,{Jane,jane@example.com}"</js>);
343 	 *
344 	 * 	<jc>// Test collection properties within beans</jc>
345 	 * 	assertBeans(cartList, <js>"items{0{name}},total"</js>,
346 	 * 		<js>"{{Laptop}},999.99"</js>,
347 	 * 		<js>"{{Phone}},599.99"</js>);
348 	 * </p>
349 	 *
350 	 * <h5 class='section'>Validation Testing:</h5>
351 	 * <p class='bjava'>
352 	 * 	<jc>// Test validation results</jc>
353 	 * 	assertBeans(validationErrors, <js>"field,message,code"</js>,
354 	 * 		<js>"email,Invalid email format,E001"</js>,
355 	 * 		<js>"age,Must be 18 or older,E002"</js>);
356 	 * </p>
357 	 *
358 	  * <h5 class='section'>Collection Iteration Testing:</h5>
359 	 * <p class='bjava'>
360 	 * 	<jc>// Test collection iteration within beans (#{...} syntax)</jc>
361 	 * 	assertBeans(departmentList, <js>"name,employees{#{name}}"</js>,
362 	 * 		<js>"Engineering,[{Alice},{Bob},{Charlie}]"</js>,
363 	 * 		<js>"Marketing,[{David},{Eve}]"</js>);
364 	 * </p>
365 	 *
366 	 * <h5 class='section'>Parser Result Testing:</h5>
367 	 * <p class='bjava'>
368 	 * 	<jc>// Test parsed object collections</jc>
369 	 * 	var parsed = JsonParser.DEFAULT.parse(jsonArray, MyBean[].class);
370 	 * 	assertBeans(Arrays.asList(parsed), <js>"prop1,prop2"</js>,
371 	 * 		<js>"val1,val2"</js>, <js>"val3,val4"</js>);
372 	 * </p>
373 	 *
374 	 * @param listOfBeans The collection of beans to check. Must not be null.
375 	 * @param fields A comma-delimited list of bean property names (supports nested syntax).
376 	 * @param values Array of expected value strings, one per bean. Each string contains comma-delimited values matching the fields.
377 	 * @throws AssertionError if the collection size doesn't match values array length or if any bean properties don't match
378 	 * @see #assertBean(Object, String, String)
379 	 */
380 	public static void assertBeans(Object actual, String fields, String...expected) {
381 		BctAssertions.assertBeans(actual, fields, expected);
382 	}
383 
384 
385 	/**
386 	 * Asserts that a List contains the expected values using flexible comparison logic.
387 	 *
388 	 * <p>This is the primary method for testing all collection-like types. For non-List collections, use
389 	 * {@link #l(Object)} to convert them to Lists first. This unified approach eliminates the need for
390 	 * separate assertion methods for arrays, sets, and other collection types.</p>
391 	 *
392 	 * <h5 class='section'>Testing Non-List Collections:</h5>
393 	 * <p class='bjava'>
394 	 * 	<jc>// Test a Set using l() conversion</jc>
395 	 * 	Set&lt;String&gt; <jv>mySet</jv> = Set.of(<js>"a"</js>, <js>"b"</js>, <js>"c"</js>);
396 	 * 	assertList(l(<jv>mySet</jv>), <js>"a"</js>, <js>"b"</js>, <js>"c"</js>);
397 	 *
398 	 * 	<jc>// Test an array using l() conversion</jc>
399 	 * 	String[] <jv>myArray</jv> = {<js>"x"</js>, <js>"y"</js>, <js>"z"</js>};
400 	 * 	assertList(l(<jv>myArray</jv>), <js>"x"</js>, <js>"y"</js>, <js>"z"</js>);
401 	 *
402 	 * 	<jc>// Test a Stream using l() conversion</jc>
403 	 * 	Stream&lt;String&gt; <jv>myStream</jv> = Stream.of(<js>"foo"</js>, <js>"bar"</js>);
404 	 * 	assertList(l(<jv>myStream</jv>), <js>"foo"</js>, <js>"bar"</js>);
405 	 * </p>
406 	 *
407 	 * <h5 class='section'>Comparison Modes:</h5>
408 	 * <p>The method supports three different ways to compare expected vs actual values:</p>
409 	 *
410 	 * <h6 class='section'>1. String Comparison (Readable Format):</h6>
411 	 * <p class='bjava'>
412 	 * 	<jc>// Elements are converted to {@link Utils#r readable} format and compared as strings</jc>
413 	 * 	assertList(List.of(1, 2, 3), <js>"1"</js>, <js>"2"</js>, <js>"3"</js>);
414 	 * 	assertList(List.of("a", "b"), <js>"a"</js>, <js>"b"</js>);
415 	 * </p>
416 	 *
417 	 * <h6 class='section'>2. Predicate Testing (Functional Validation):</h6>
418 	 * <p class='bjava'>
419 	 * 	<jc>// Use Predicate&lt;T&gt; for functional testing</jc>
420 	 * 	Predicate&lt;Integer&gt; <jv>greaterThanOne</jv> = <jv>x</jv> -&gt; <jv>x</jv> &gt; 1;
421 	 * 	assertList(List.of(2, 3, 4), <jv>greaterThanOne</jv>, <jv>greaterThanOne</jv>, <jv>greaterThanOne</jv>);
422 	 *
423 	 * 	<jc>// Mix predicates with other comparison types</jc>
424 	 * 	Predicate&lt;String&gt; <jv>startsWithA</jv> = <jv>s</jv> -&gt; <jv>s</jv>.startsWith(<js>"a"</js>);
425 	 * 	assertList(List.of(<js>"apple"</js>, <js>"banana"</js>), <jv>startsWithA</jv>, <js>"banana"</js>);
426 	 * </p>
427 	 *
428 	 * <h6 class='section'>3. Object Equality (Direct Comparison):</h6>
429 	 * <p class='bjava'>
430 	 * 	<jc>// Non-String, non-Predicate objects use Objects.equals() comparison</jc>
431 	 * 	assertList(List.of(1, 2, 3), 1, 2, 3); <jc>// Integer objects</jc>
432 	 * 	assertList(List.of(<jv>myBean1</jv>, <jv>myBean2</jv>), <jv>myBean1</jv>, <jv>myBean2</jv>); <jc>// Custom objects</jc>
433 	 * </p>
434 	 *
435 	 * @param actual The List to test. Must not be null. For other collection types, use {@link #l(Object)} to convert first.
436 	 * @param expected Multiple arguments of expected values.
437 	 *                 Can be Strings (readable format comparison), Predicates (functional testing), or Objects (direct equality).
438 	 * @throws AssertionError if the List size or contents don't match expected values
439 	 * @see #l(Object) for converting other collection types to Lists
440 	 */
441 	public static <T> void assertList(Object actual, Object...expected) {
442 		BctAssertions.assertList(actual, expected);
443 	}
444 
445 	/**
446 	 * Asserts that a Map contains the expected key/value pairs using flexible comparison logic.
447 	 *
448 	 * <p>This is a passthrough method to {@link BctAssertions#assertMap(Map, Object...)}.
449 	 * Map entries are serialized to strings as key/value pairs in the format <js>"key=value"</js>.
450 	 * Nested maps and collections are supported with appropriate formatting.</p>
451 	 *
452 	 * <h5 class='section'>Usage Examples:</h5>
453 	 * <p class='bjava'>
454 	 *    <jc>// Test simple map entries</jc>
455 	 *    Map&lt;String,String&gt; <jv>simpleMap</jv> = Map.<jsm>of</jsm>(<js>"a"</js>, <js>"1"</js>, <js>"b"</js>, <js>"2"</js>);
456 	 *    <jsm>assertMap</jsm>(<jv>simpleMap</jv>, <js>"a=1"</js>, <js>"b=2"</js>);
457 	 *
458 	 *    <jc>// Test nested maps</jc>
459 	 *    Map&lt;String,Map&lt;String,Integer&gt;&gt; <jv>nestedMap</jv> = Map.<jsm>of</jsm>(<js>"a"</js>, Map.<jsm>of</jsm>(<js>"b"</js>, 1));
460 	 *    <jsm>assertMap</jsm>(<jv>nestedMap</jv>, <js>"a={b=1}"</js>);
461 	 *
462 	 *    <jc>// Test maps with arrays/collections</jc>
463 	 *    Map&lt;String,Map&lt;String,Integer[]&gt;&gt; <jv>mapWithArrays</jv> = Map.<jsm>of</jsm>(<js>"a"</js>, Map.<jsm>of</jsm>(<js>"b"</js>, <jk>new</jk> Integer[]{1,2}));
464 	 *    <jsm>assertMap</jsm>(<jv>mapWithArrays</jv>, <js>"a={b=[1,2]}"</js>);
465 	 * </p>
466 	 *
467 	 * @param actual The Map to test. Must not be null.
468 	 * @param expected Multiple arguments of expected map entries.
469 	 *                 Can be Strings (readable format comparison), Predicates (functional testing), or Objects (direct equality).
470 	 * @throws AssertionError if the Map size or contents don't match expected values
471 	 * @see BctAssertions#assertMap(Map, Object...)
472 	 */
473 	public static void assertMap(Map<?,?> actual, Object...expected) {
474 		BctAssertions.assertMap(actual, expected);
475 	}
476 
477 	/**
478 	 * Asserts an object matches the expected string after it's been made {@link Utils#r readable}.
479 	 */
480 	public static void assertContains(String expected, Object actual) {
481 		BctAssertions.assertContains(expected, actual);
482 	}
483 
484 	/**
485 	 * Similar to {@link #assertContains(String, Object)} but allows the expected to be a comma-delimited list of strings that
486 	 * all must match.
487 	 * @param expected
488 	 * @param actual
489 	 */
490 	public static void assertContainsAll(Object actual, String...expected) {
491 		BctAssertions.assertContainsAll(actual, expected);
492 	}
493 
494 	/**
495 	 * Asserts that a collection is not null and empty.
496 	 */
497 	public static void assertEmpty(Object value) {
498 		BctAssertions.assertEmpty(value);
499 	}
500 
501 	public static void assertEqualsAll(Object...values) {
502 		for (var i = 1; i < values.length; i++) {
503 			assertEquals(values[0], values[i], fms("Elements at index {0} and {1} did not match. {0}={2}, {1}={3}", 0, i, r(values[0]), r(values[i])));
504 		}
505 	}
506 
507 	/**
508 	 * Asserts the JSON5 representation of the specified object.
509 	 */
510 	public static void assertJson(String expected, Object value) {
511 		assertEquals(expected, Json5.DEFAULT_SORTED.write(value));
512 	}
513 
514 	/**
515 	 * Converts the specified object to a string and then replaces any newlines with pipes for easy comparison during testing.
516 	 * @param value
517 	 * @return
518 	 */
519 	public static String pipedLines(Object value) {
520 		return r(value).replaceAll("\\r?\\n", "|");
521 	}
522 
523 	/**
524 	 * Asserts that the values in the specified map are the specified values after being converted to {@link Utils#r readable} strings.
525 	 *
526 	 * <p>This method works identically to {@link #assertBean(Object, String, String)} but is optimized for Java Maps.
527 	 * It supports the same nested property syntax and value formatting rules as <c>assertBean</c>.</p>
528 	 *
529 	 * <h5 class='section'>Basic Map Testing:</h5>
530 	 * <p class='bjava'>
531 	 * 	<jc>// Test map entries</jc>
532 	 * 	assertMap(myMap, <js>"key1,key2,key3"</js>, <js>"val1,val2,val3"</js>);
533 	 *
534 	 * 	<jc>// Test single map entry</jc>
535 	 * 	assertMap(myMap, <js>"status"</js>, <js>"active"</js>);
536 	 * </p>
537 	 *
538 	 * <h5 class='section'>Nested Object Testing in Maps:</h5>
539 	 * <p class='bjava'>
540 	 * 	<jc>// Test nested objects within map values</jc>
541 	 * 	assertMap(myMap, <js>"user{name,email}"</js>, <js>"{John,john@example.com}"</js>);
542 	 *
543 	 * 	<jc>// Test class properties of map values</jc>
544 	 * 	assertMap(myMap, <js>"items{class{simpleName}}"</js>, <js>"{{ArrayList}}"</js>);
545 	 * </p>
546 	 *
547 	 * <h5 class='section'>Collection Testing in Maps:</h5>
548 	 * <p class='bjava'>
549 	 * 	<jc>// Test array/list values in maps</jc>
550 	 * 	assertMap(myMap, <js>"tags{0,1},count"</js>, <js>"{red,blue},2"</js>);
551 	 *
552 	 * 	<jc>// Test nested properties within collection elements</jc>
553 	 * 	assertMap(myMap, <js>"orders{0{id,total}}"</js>, <js>"{{123,99.95}}"</js>);
554 	 * </p>
555 	 *
556 	 * <h5 class='section'>BeanMap and JsonMap Usage:</h5>
557 	 * <p class='bjava'>
558 	 * 	<jc>// Excellent for testing BeanMap instances</jc>
559 	 * 	var beanMap = BeanContext.DEFAULT.toBeanMap(myBean);
560 	 * 	assertMap(beanMap, <js>"name,age,active"</js>, <js>"John,30,true"</js>);
561 	 *
562 	 * 	<jc>// Test JsonMap parsing results</jc>
563 	 * 	var jsonMap = JsonMap.ofJson(<js>"{foo:'bar', baz:123}"</js>);
564 	 * 	assertMap(jsonMap, <js>"foo,baz"</js>, <js>"bar,123"</js>);
565 	 * </p>
566 	 *
567 	 * @param actual The Map object to test. Must not be null.
568 	 * @param fields Comma-delimited list of map keys to test. Supports nested syntax with {}.
569 	 * @param expected Comma-delimited list of expected values. Must match the order of fields.
570 	 * @throws NullPointerException if the map is null
571 	 * @throws AssertionError if any map values don't match expected values
572 	 * @see #assertBean(Object, String, String)
573 	 * @see BeanConverter
574 	 * @see BasicBeanConverter
575 	 */
576 	public static void assertMap(Map<?,?> actual, String fields, String expected) {
577 		BctAssertions.assertBean(actual, fields, expected);
578 	}
579 
580 	/**
581 	 * Asserts that mapped property access on an object returns expected values using a custom BiFunction.
582 	 *
583 	 * <p>This is the most powerful and flexible BCT method, designed for testing objects that don't follow
584 	 * standard JavaBean patterns or require custom property access logic. The BiFunction allows complete
585 	 * control over how properties are retrieved from the target object.</p>
586 	 *
587 	 * <p>When the BiFunction throws an exception, it's automatically caught and the exception's
588 	 * simple class name becomes the property value for comparison (e.g., "NullPointerException").</p>
589 	 *
590 	 * <p>This method creates an intermediate LinkedHashMap to collect all property values before
591 	 * delegating to assertMap(Map, String, String). This ensures consistent ordering
592 	 * and supports the full nested property syntax. The {@link BasicBeanConverter#DEFAULT} is used
593 	 * for value stringification and nested property access.</p>
594 	 *
595 	 * @param <T> The type of object being tested
596 	 * @param actual The object to test properties on
597 	 * @param f The BiFunction that extracts property values. Receives (object, propertyName) and returns the property value.
598 	 * @param properties Comma-delimited list of property names to test
599 	 * @param expected Comma-delimited list of expected values (exceptions become simple class names)
600 	 * @throws AssertionError if any mapped property values don't match expected values
601 	 * @see #assertBean(Object, String, String)
602 	 * @see #assertMap(Map, String, String)
603 	 * @see BeanConverter
604 	 * @see BasicBeanConverter
605 	 */
606 	public static <T> void assertMapped(T actual, BiFunction<T,String,Object> f, String properties, String expected) {
607 		BctAssertions.assertMapped(actual, f, properties, expected);
608 	}
609 
610 	/**
611 	 * Asserts that a collection is not null and not empty.
612 	 */
613 	public static void assertNotEmpty(Object value) {
614 		BctAssertions.assertNotEmpty(value);
615 	}
616 
617 	public static void assertNotEqualsAny(Object actual, Object...values) {
618 		assertNotNull(actual, "Value was null.");
619 		for (var i = 0; i < values.length; i++) {
620 			assertNotEquals(values[i], actual, fms("Element at index {0} unexpectedly matched.  expected={1}, actual={2}", i, values[i], s(actual)));
621 		}
622 	}
623 
624 	/**
625 	 * Asserts the serialized representation of the specified object.
626 	 */
627 	public static void assertSerialized(Object actual, WriterSerializer s, String expected) {
628 		assertEquals(expected, s.toString(actual));
629 	}
630 
631 	/**
632 	 * Asserts that a collection-like object or string is not null and of the specified size.
633 	 *
634 	 * <p>This method can validate the size of various types of objects:</p>
635 	 * <ul>
636 	 * 	<li><b>String:</b> Validates character length</li>
637 	 * 	<li><b>Collection-like objects:</b> Any object that can be converted to a List via {@link #toList(Object)}</li>
638 	 * </ul>
639 	 *
640 	 * <h5 class='section'>Usage Examples:</h5>
641 	 * <p class='bjava'>
642 	 * 	<jc>// Test string length</jc>
643 	 * 	assertSize(5, <js>"hello"</js>);
644 	 *
645 	 * 	<jc>// Test collection size</jc>
646 	 * 	assertSize(3, List.of(<js>"a"</js>, <js>"b"</js>, <js>"c"</js>));
647 	 *
648 	 * 	<jc>// Test array size</jc>
649 	 * 	assertSize(2, <jk>new</jk> String[]{<js>"x"</js>, <js>"y"</js>});
650 	 * </p>
651 	 *
652 	 * @param expected The expected size/length.
653 	 * @param actual The object to test. Must not be null.
654 	 * @throws AssertionError if the object is null or not the expected size.
655 	 */
656 	public static void assertSize(int expected, Object actual) {
657 		BctAssertions.assertSize(expected, actual);
658 	}
659 
660 	/**
661 	 * Asserts an object matches the expected string after it's been made {@link Utils#r readable}.
662 	 */
663 	public static void assertString(String expected, Object actual) {
664 		BctAssertions.assertString(expected, actual);
665 	}
666 
667 	/**
668 	 * Asserts value when stringified matches the specified glob-style pattern.
669 	 */
670 	public static void assertMatchesGlob(String pattern, Object value) {
671 		BctAssertions.assertMatchesGlob(pattern, value);
672 	}
673 
674 	/**
675 	 * Asserts an object matches the expected string after it's been made {@link Utils#r readable}.
676 	 */
677 	public static void assertString(String expected, Object actual, Supplier<String> messageSupplier) {
678 		BctAssertions.assertString(expected, actual);
679 	}
680 
681 	public static <T extends Throwable> T assertThrowable(Class<? extends Throwable> expectedType, String expectedSubstring, T t) {
682 		var messages = getMessages(t);
683 		assertTrue(messages.contains(expectedSubstring), fms("Expected message to contain: {0}.\nActual:\n{1}", expectedSubstring, messages));
684 		return t;
685 	}
686 
687 	public static <T extends Throwable> T assertThrowsWithMessage(Class<T> expectedType, List<String> expectedSubstrings, org.junit.jupiter.api.function.Executable executable) {
688 		var exception = Assertions.assertThrows(expectedType, executable);
689 		var messages = getMessages(exception);
690 		expectedSubstrings.stream().forEach(x -> assertTrue(messages.contains(x), fms("Expected message to contain: {0}.\nActual:\n{1}", x, messages)));
691 		return exception;
692 	}
693 
694 	public static <T extends Throwable> T assertThrowsWithMessage(Class<T> expectedType, String expectedSubstring, org.junit.jupiter.api.function.Executable executable) {
695 		var exception = Assertions.assertThrows(expectedType, executable);
696 		var messages = getMessages(exception);
697 		assertTrue(messages.contains(expectedSubstring), fms("Expected message to contain: {0}.\nActual:\n{1}", expectedSubstring, messages));
698 		return exception;
699 	}
700 
701 	/**
702 	 * Validates that the whitespace is correct in the specified XML.
703 	 */
704 	public static final void checkXmlWhitespace(String out) throws SerializeException {
705 		if (out.indexOf('\u0000') != -1) {
706 			for (var s : out.split("\u0000"))
707 				checkXmlWhitespace(s);
708 			return;
709 		}
710 
711 		var indent = -1;
712 		var startTag = Pattern.compile("^(\\s*)<[^/>]+(\\s+\\S+=['\"]\\S*['\"])*\\s*>$");  // NOSONAR
713 		var endTag = Pattern.compile("^(\\s*)</[^>]+>$");
714 		var combinedTag = Pattern.compile("^(\\s*)<[^>/]+(\\s+\\S+=['\"]\\S*['\"])*\\s*/>$");  // NOSONAR
715 		var contentOnly = Pattern.compile("^(\\s*)[^\\s\\<]+$");
716 		var tagWithContent = Pattern.compile("^(\\s*)<[^>]+>.*</[^>]+>$");
717 		var lines = out.split("\n");
718 		try {
719 			for (var i = 0; i < lines.length; i++) {
720 				var line = lines[i];
721 				var m = startTag.matcher(line);
722 				if (m.matches()) {
723 					indent++;
724 					if (m.group(1).length() != indent)
725 						throw new SerializeException("Wrong indentation detected on start tag line ''{0}''", i+1);
726 					continue;
727 				}
728 				m = endTag.matcher(line);
729 				if (m.matches()) {
730 					if (m.group(1).length() != indent)
731 						throw new SerializeException("Wrong indentation detected on end tag line ''{0}''", i+1);
732 					indent--;
733 					continue;
734 				}
735 				m = combinedTag.matcher(line);
736 				if (m.matches()) {
737 					indent++;
738 					if (m.group(1).length() != indent)
739 						throw new SerializeException("Wrong indentation detected on combined tag line ''{0}''", i+1);
740 					indent--;
741 					continue;
742 				}
743 				m = contentOnly.matcher(line);
744 				if (m.matches()) {
745 					indent++;
746 					if (m.group(1).length() != indent)
747 						throw new SerializeException("Wrong indentation detected on content-only line ''{0}''", i+1);
748 					indent--;
749 					continue;
750 				}
751 				m = tagWithContent.matcher(line);
752 				if (m.matches()) {
753 					indent++;
754 					if (m.group(1).length() != indent)
755 						throw new SerializeException("Wrong indentation detected on tag-with-content line ''{0}''", i+1);
756 					indent--;
757 					continue;
758 				}
759 				throw new SerializeException("Unmatched whitespace line at line number ''{0}''", i+1);
760 			}
761 			if (indent != -1)
762 				throw new SerializeException("Possible unmatched tag.  indent=''{0}''", indent);
763 		} catch (SerializeException e) {
764 			printLines(lines);
765 			throw e;
766 		}
767 	}
768 
769 	/**
770 	 * Returns the value of the specified field/property on the specified object.
771 	 * First looks for getter, then looks for field.
772 	 * Methods and fields can be any visibility.
773 	 */
774 	public static Object getBeanProp(Object o, String name) {
775 		return safe(() -> {
776 			var f = (Field)null;
777 			var c = o.getClass();
778 			var n = Character.toUpperCase(name.charAt(0)) + name.substring(1);
779 			var m = Arrays.stream(c.getMethods()).filter(x -> x.getName().equals("is"+n) && x.getParameterCount() == 0 && x.getAnnotation(BeanIgnore.class) == null).findFirst().orElse(null);
780 			if (m != null) {
781 				m.setAccessible(true);
782 				return m.invoke(o);
783 			}
784 			m = Arrays.stream(c.getMethods()).filter(x -> x.getName().equals("get"+n) && x.getParameterCount() == 0 && x.getAnnotation(BeanIgnore.class) == null).findFirst().orElse(null);
785 			if (m != null) {
786 				m.setAccessible(true);
787 				return m.invoke(o);
788 			}
789 			m = Arrays.stream(c.getMethods()).filter(x -> x.getName().equals("get") && x.getParameterCount() == 1 && x.getParameterTypes()[0] == String.class && x.getAnnotation(BeanIgnore.class) == null).findFirst().orElse(null);
790 			if (m != null) {
791 				m.setAccessible(true);
792 				return m.invoke(o, name);
793 			}
794 			var c2 = c;
795 			while (f == null && c2 != null) {
796 				f = Arrays.stream(c2.getDeclaredFields()).filter(x -> x.getName().equals(name)).findFirst().orElse(null);
797 				c2 = c2.getSuperclass();
798 			}
799 			if (f != null) {
800 				f.setAccessible(true);
801 				return f.get(o);
802 			}
803 			m = Arrays.stream(c.getMethods()).filter(x -> x.getName().equals(name) && x.getParameterCount() == 0).findFirst().orElse(null);
804 			if (m != null) {
805 				m.setAccessible(true);
806 				return m.invoke(o);
807 			}
808 			throw runtimeException("Property {0} not found on object of type {1}", name, classNameOf(o));
809 		});
810 	}
811 
812 	private static String getMessages(Throwable t) {
813 		return Stream.iterate(t, Throwable::getCause).takeWhile(e -> e != null).map(Throwable::getMessage).collect(joining("\n"));
814 	}
815 
816 	/**
817 	 * Gets the swagger for the specified @Resource-annotated object.
818 	 * @param c
819 	 * @return
820 	 */
821 	public static Swagger getSwagger(Class<?> c) {
822 		try {
823 			var r = c.getDeclaredConstructor().newInstance();
824 			var rc = RestContext.create(r.getClass(),null,null).init(()->r).build();
825 			var ctx = RestOpContext.create(TestUtils.class.getMethod("getSwagger", Class.class), rc).build();
826 			var session = RestSession.create(rc).resource(r).req(new MockServletRequest()).res(new MockServletResponse()).build();
827 			var req = ctx.createRequest(session);
828 			var ip = rc.getSwaggerProvider();
829 			return ip.getSwagger(rc, req.getLocale());
830 		} catch (Exception e) {
831 			throw new RuntimeException(e);
832 		}
833 	}
834 
835 	/**
836 	 * Creates an input stream from the specified string.
837 	 *
838 	 * @param in The contents of the reader.
839 	 * @return A new input stream.
840 	 */
841 	public static final ByteArrayInputStream inputStream(String in) {
842 		return new ByteArrayInputStream(in.getBytes());
843 	}
844 
845 	public static String json(Object o) {
846 		return Json5.DEFAULT_SORTED.write(o);
847 	}
848 
849 	public static <T> T json(String o, Class<T> c) {
850 		return safe(()->Json5.DEFAULT_SORTED.read(o, c));
851 	}
852 
853 	public static <T> T jsonRoundTrip(T o, Class<T> c) {
854 		return json(json(o), c);
855 	}
856 
857 	/**
858 	 * Creates a reader from the specified string.
859 	 *
860 	 * @param in The contents of the reader.
861 	 * @return A new reader.
862 	 */
863 	public static final StringReader reader(String in) {
864 		return new StringReader(in);
865 	}
866 
867 	/**
868 	 * Temporarily sets the default system locale to the specified locale.
869 	 * Use {@link #unsetLocale()} to unset it.
870 	 *
871 	 * @param name
872 	 */
873 	public static final void setLocale(Locale v) {
874 		SYSTEM_LOCALE.set(Locale.getDefault());
875 		Locale.setDefault(v);
876 	}
877 
878 	/**
879 	 * Temporarily sets the default system timezone to the specified timezone ID.
880 	 * Use {@link #unsetTimeZone()} to unset it.
881 	 *
882 	 * @param name
883 	 */
884 	public static final synchronized void setTimeZone(String v) {
885 		SYSTEM_TIME_ZONE.set(TimeZone.getDefault());
886 		TimeZone.setDefault(TimeZone.getTimeZone(v));
887 	}
888 
889 	public static final void unsetLocale() {
890 		Locale.setDefault(SYSTEM_LOCALE.get());
891 	}
892 
893 	public static final synchronized void unsetTimeZone() {
894 		TimeZone.setDefault(SYSTEM_TIME_ZONE.get());
895 	}
896 
897 	/**
898 	 * Constructs a {@link URL} object from a string.
899 	 */
900 	public static URL url(String value) {
901 		return safe(()->new URI(value).toURL());
902 	}
903 
904 	/**
905 	 * Test whitespace and generated schema.
906 	 */
907 	public static final void validateXml(Object o) throws Exception {
908 		validateXml(o, XmlSerializer.DEFAULT_NS_SQ);
909 	}
910 
911 	/**
912 	 * Test whitespace and generated schema.
913 	 */
914 	public static final void validateXml(Object o, XmlSerializer s) throws Exception {
915 		s = s.copy().ws().ns().addNamespaceUrisToRoot().build();
916 		var xml = s.serialize(o);
917 		checkXmlWhitespace(xml);
918 	}
919 
920 	public static final <T> BeanTester<T> testBean(T bean) {
921 		return (BeanTester<T>) new BeanTester<>().bean(bean);
922 	}
923 
924 	/**
925 	 * Extracts HTML/XML elements from a string based on element name and attributes.
926 	 *
927 	 * <p>Uses a depth-tracking parser to handle nested elements correctly, even with malformed HTML.</p>
928 	 *
929 	 * <h5 class='section'>Examples:</h5>
930 	 * <pre>
931 	 * // Extract all div elements with class='tag-block'
932 	 * List&lt;String&gt; blocks = extractXml(html, "div", Map.of("class", "tag-block"));
933 	 *
934 	 * // Extract all span elements (no attribute filtering)
935 	 * List&lt;String&gt; spans = extractXml(html, "span", null);
936 	 *
937 	 * // Extract divs with multiple attributes
938 	 * List&lt;String&gt; divs = extractXml(html, "div", Map.of("class", "header", "id", "main"));
939 	 * </pre>
940 	 *
941 	 * @param html The HTML/XML content to parse
942 	 * @param elementName The element name to extract (e.g., "div", "span")
943 	 * @param withAttributes Optional map of attribute name/value pairs that must match.
944 	 *                       Pass null or empty map to match all elements of the given name.
945 	 * @return List of HTML content strings (inner content of matching elements)
946 	 */
947 	public static List<String> extractXml(String html, String elementName, Map<String,String> withAttributes) {
948 		List<String> results = new ArrayList<>();
949 
950 		if (html == null || elementName == null) {
951 			return results;
952 		}
953 
954 		// Find all opening tags of the specified element
955 		String openTag = "<" + elementName;
956 		int searchPos = 0;
957 
958 		while ((searchPos = html.indexOf(openTag, searchPos)) != -1) {
959 			// Find the end of the opening tag
960 			int tagEnd = html.indexOf('>', searchPos);
961 			if (tagEnd == -1) break;
962 
963 			String fullOpenTag = html.substring(searchPos, tagEnd + 1);
964 
965 			// Check if attributes match
966 			boolean matches = true;
967 			if (withAttributes != null && !withAttributes.isEmpty()) {
968 				for (Map.Entry<String, String> entry : withAttributes.entrySet()) {
969 					String attrName = entry.getKey();
970 					String attrValue = entry.getValue();
971 
972 					// Look for attribute in the tag (handle both single and double quotes)
973 					String pattern1 = attrName + "=\"" + attrValue + "\"";
974 					String pattern2 = attrName + "='" + attrValue + "'";
975 
976 
977 					if (!fullOpenTag.contains(pattern1) && !fullOpenTag.contains(pattern2)) {
978 						matches = false;
979 						break;
980 					}
981 				}
982 			}
983 
984 			if (matches) {
985 				// Find matching closing tag by tracking depth
986 				int contentStart = tagEnd + 1;
987 				int depth = 1;
988 				int pos = contentStart;
989 
990 				while (pos < html.length() && depth > 0) {
991 					// Look for next opening or closing tag of same element
992 					int nextOpen = html.indexOf("<" + elementName, pos);
993 					int nextClose = html.indexOf("</" + elementName + ">", pos);
994 
995 					// Validate that nextOpen is actually an opening tag
996 					if (nextOpen != -1 && (nextOpen < nextClose || nextClose == -1)) {
997 						if (nextOpen + elementName.length() + 1 < html.length()) {
998 							char nextChar = html.charAt(nextOpen + elementName.length() + 1);
999 							if (nextChar == ' ' || nextChar == '>' || nextChar == '/') {
1000 								depth++;
1001 								pos = nextOpen + elementName.length() + 1;
1002 								continue;
1003 							}
1004 						}
1005 						// Not a valid opening tag, skip it
1006 						pos = nextOpen + 1;
1007 						continue;
1008 					}
1009 
1010 					if (nextClose != -1) {
1011 						depth--;
1012 						if (depth == 0) {
1013 							// Found matching close tag
1014 							results.add(html.substring(contentStart, nextClose));
1015 							break;
1016 						}
1017 						pos = nextClose + elementName.length() + 3;
1018 					} else {
1019 						// No more closing tags
1020 						break;
1021 					}
1022 				}
1023 			}
1024 
1025 			searchPos = tagEnd + 1;
1026 		}
1027 
1028 		return results;
1029 	}
1030 }