001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.juneau.junit.bct; 018 019import static java.util.Collections.*; 020import static java.util.Spliterators.*; 021import static java.util.stream.StreamSupport.*; 022import static org.apache.juneau.commons.utils.CollectionUtils.*; 023import static org.apache.juneau.commons.utils.Utils.*; 024import static org.apache.juneau.junit.bct.BctConfiguration.*; 025 026import java.util.*; 027import java.util.stream.*; 028 029/** 030 * Collection of standard listifier implementations for the Bean-Centric Testing framework. 031 * 032 * <p>This class provides built-in list conversion strategies that handle common Java collection 033 * and iterable types. These listifiers are automatically registered when using 034 * {@link BasicBeanConverter.Builder#defaultSettings()}.</p> 035 * 036 * <h5 class='section'>Purpose:</h5> 037 * <p>Listifiers convert various collection-like objects to {@link List} instances for uniform 038 * processing in BCT assertions. This enables consistent iteration and element access across 039 * different data structures like arrays, iterators, streams, and custom collections.</p> 040 * 041 * <h5 class='section'>Built-in Listifiers:</h5> 042 * <ul> 043 * <li><b>{@link #collectionListifier()}</b> - Converts {@link Collection} objects to {@link ArrayList}</li> 044 * <li><b>{@link #iterableListifier()}</b> - Converts {@link Iterable} objects using streams</li> 045 * <li><b>{@link #iteratorListifier()}</b> - Converts {@link Iterator} objects to lists (consumes iterator)</li> 046 * <li><b>{@link #enumerationListifier()}</b> - Converts {@link Enumeration} objects to lists</li> 047 * <li><b>{@link #streamListifier()}</b> - Converts {@link Stream} objects to lists (terminates stream)</li> 048 * <li><b>{@link #mapListifier()}</b> - Converts {@link Map} to list of {@link java.util.Map.Entry} objects</li> 049 * </ul> 050 * 051 * <h5 class='section'>Usage Example:</h5> 052 * <p class='bjava'> 053 * <jc>// Register listifiers using builder</jc> 054 * <jk>var</jk> <jv>converter</jv> = BasicBeanConverter.<jsm>builder</jsm>() 055 * .defaultSettings() 056 * .addListifier(Collection.<jk>class</jk>, Listifiers.<jsm>collectionListifier</jsm>()) 057 * .addListifier(Stream.<jk>class</jk>, Listifiers.<jsm>streamListifier</jsm>()) 058 * .build(); 059 * </p> 060 * 061 * <h5 class='section'>Iterator Consumption:</h5> 062 * <p><b>Warning:</b> Some listifiers consume their input objects during conversion:</p> 063 * <ul> 064 * <li><b>{@link Iterator}:</b> Elements are consumed and iterator becomes exhausted</li> 065 * <li><b>{@link Enumeration}:</b> Elements are consumed and enumeration becomes exhausted</li> 066 * <li><b>{@link Stream}:</b> Stream is terminated and cannot be reused</li> 067 * </ul> 068 * 069 * <h5 class='section'>Custom Listifier Development:</h5> 070 * <p>When creating custom listifiers, follow these patterns:</p> 071 * <ul> 072 * <li><b>Null Safety:</b> Handle <jk>null</jk> inputs gracefully</li> 073 * <li><b>Immutability:</b> Return new lists rather than modifying inputs</li> 074 * <li><b>Type Safety:</b> Ensure proper generic type handling</li> 075 * <li><b>Performance:</b> Consider memory usage for large collections</li> 076 * </ul> 077 * 078 * @see Listifier 079 * @see BasicBeanConverter.Builder#addListifier(Class, Listifier) 080 * @see BasicBeanConverter.Builder#defaultSettings() 081 */ 082@SuppressWarnings("rawtypes") 083public class Listifiers { 084 085 /** 086 * Returns a listifier for {@link Collection} objects that converts them to {@link ArrayList}. 087 * 088 * <p>This listifier creates a new ArrayList containing all elements from the source collection. 089 * It works with any Collection subtype including List, Set, Queue, and Deque.</p> 090 * 091 * <h5 class='section'>Behavior:</h5> 092 * <ul> 093 * <li><b>Non-empty collections:</b> Returns new ArrayList with all elements in iteration order</li> 094 * <li><b>Empty collections:</b> Returns new empty ArrayList</li> 095 * <li><b>Preserves order:</b> Maintains the iteration order of the source collection</li> 096 * <li><b>Set ordering:</b> Converts unordered Sets (HashSet, etc.) to TreeSet for deterministic ordering</li> 097 * </ul> 098 * 099 * <h5 class='section'>Set Ordering Behavior:</h5> 100 * <p>To ensure predictable test results, this listifier handles Sets with unreliable ordering:</p> 101 * <ul> 102 * <li><b>{@link SortedSet} (TreeSet, etc.):</b> Preserves existing sort order</li> 103 * <li><b>{@link LinkedHashSet}:</b> Preserves insertion order</li> 104 * <li><b>{@link HashSet} and other unordered Sets:</b> Converts to {@link TreeSet} for natural ordering</li> 105 * </ul> 106 * 107 * <h5 class='section'>Usage Examples:</h5> 108 * <p class='bjava'> 109 * <jc>// Test with different collection types</jc> 110 * <jk>var</jk> <jv>list</jv> = List.<jsm>of</jsm>(<js>"a"</js>, <js>"b"</js>, <js>"c"</js>); 111 * <jsm>assertList</jsm>(<jv>list</jv>, <js>"a"</js>, <js>"b"</js>, <js>"c"</js>); 112 * 113 * <jc>// HashSet converted to TreeSet for predictable ordering</jc> 114 * <jk>var</jk> <jv>set</jv> = Set.<jsm>of</jsm>(<js>"z"</js>, <js>"a"</js>, <js>"m"</js>); 115 * <jsm>assertList</jsm>(<jv>set</jv>, <js>"a"</js>, <js>"m"</js>, <js>"z"</js>); <jc>// Natural ordering</jc> 116 * 117 * <jc>// LinkedHashSet preserves insertion order</jc> 118 * <jk>var</jk> <jv>linkedSet</jv> = <jk>new</jk> LinkedHashSet<>(Arrays.<jsm>asList</jsm>(<js>"first"</js>, <js>"second"</js>)); 119 * <jsm>assertList</jsm>(<jv>linkedSet</jv>, <js>"first"</js>, <js>"second"</js>); 120 * 121 * <jk>var</jk> <jv>queue</jv> = <jk>new</jk> LinkedList<>(Arrays.<jsm>asList</jsm>(<js>"first"</js>, <js>"second"</js>)); 122 * <jsm>assertList</jsm>(<jv>queue</jv>, <js>"first"</js>, <js>"second"</js>); 123 * </p> 124 * 125 * <h5 class='section'>Performance:</h5> 126 * <p>This listifier creates a new ArrayList and copies all elements, so it has O(n) time 127 * and space complexity. For unordered Sets, an additional TreeSet conversion adds O(n log n) 128 * sorting overhead. For large collections, consider the memory implications.</p> 129 * 130 * @return A {@link Listifier} for {@link Collection} objects 131 * @see Collection 132 * @see ArrayList 133 * @see TreeSet 134 * @see LinkedHashSet 135 */ 136 @SuppressWarnings("unchecked") 137 public static Listifier<Collection> collectionListifier() { 138 return (bc, collection) -> { 139 if (collection instanceof Set && ! (collection instanceof SortedSet) && ! (collection instanceof LinkedHashSet) && BctConfiguration.get(BCT_SORT_COLLECTIONS, false)) { 140 // TODO - This is too unreliable. 141 var collection2 = new TreeSet<>(flexibleComparator(bc)); 142 collection2.addAll(collection); 143 collection = collection2; 144 } 145 return toList(collection); 146 }; 147 } 148 149 /** 150 * Returns a listifier for {@link Enumeration} objects that converts them to lists. 151 * 152 * <p><b>Warning:</b> This listifier consumes the enumeration during conversion. After listification, 153 * the enumeration will be exhausted and cannot be used again.</p> 154 * 155 * <h5 class='section'>Behavior:</h5> 156 * <ul> 157 * <li><b>Element extraction:</b> Consumes all remaining elements from the enumeration</li> 158 * <li><b>Order preservation:</b> Maintains the enumeration's order in the resulting list</li> 159 * <li><b>Enumeration exhaustion:</b> The enumeration becomes unusable after conversion</li> 160 * </ul> 161 * 162 * <h5 class='section'>Usage Examples:</h5> 163 * <p class='bjava'> 164 * <jc>// Test with Vector enumeration</jc> 165 * <jk>var</jk> <jv>vector</jv> = <jk>new</jk> Vector<>(List.<jsm>of</jsm>(<js>"x"</js>, <js>"y"</js>, <js>"z"</js>)); 166 * <jk>var</jk> <jv>enumeration</jv> = <jv>vector</jv>.elements(); 167 * <jsm>assertList</jsm>(<jv>enumeration</jv>, <js>"x"</js>, <js>"y"</js>, <js>"z"</js>); 168 * 169 * <jc>// Test with Hashtable enumeration</jc> 170 * <jk>var</jk> <jv>table</jv> = <jk>new</jk> Hashtable<>(Map.<jsm>of</jsm>(<js>"key1"</js>, <js>"value1"</js>)); 171 * <jk>var</jk> <jv>keys</jv> = <jv>table</jv>.keys(); 172 * <jsm>assertList</jsm>(<jv>keys</jv>, <js>"key1"</js>); 173 * </p> 174 * 175 * @return A {@link Listifier} for {@link Enumeration} objects 176 * @see Enumeration 177 */ 178 @SuppressWarnings("unchecked") 179 public static Listifier<Enumeration> enumerationListifier() { 180 return (bc, enumeration) -> list(enumeration); 181 } 182 183 /** 184 * Returns a listifier for {@link Iterable} objects that converts them using streams. 185 * 186 * <p>This listifier handles any Iterable implementation by creating a stream from its 187 * spliterator and collecting elements to a list. It works with custom iterables and 188 * collection types not covered by more specific listifiers.</p> 189 * 190 * <h5 class='section'>Behavior:</h5> 191 * <ul> 192 * <li><b>Standard iterables:</b> Converts elements to list maintaining iteration order</li> 193 * <li><b>Custom iterables:</b> Works with any object implementing Iterable interface</li> 194 * <li><b>Large iterables:</b> Processes all elements regardless of size</li> 195 * </ul> 196 * 197 * <h5 class='section'>Usage Examples:</h5> 198 * <p class='bjava'> 199 * <jc>// Test with custom iterable</jc> 200 * <jk>var</jk> <jv>range</jv> = IntStream.<jsm>range</jsm>(<jv>1</jv>, <jv>4</jv>).<jsm>boxed</jsm>().<jsm>collect</jsm>(toList()); 201 * <jsm>assertList</jsm>(<jv>range</jv>, <jv>1</jv>, <jv>2</jv>, <jv>3</jv>); 202 * 203 * <jc>// Test with custom Iterable implementation</jc> 204 * <jk>var</jk> <jv>fibonacci</jv> = <jk>new</jk> FibonacciIterable(<jv>5</jv>); 205 * <jsm>assertList</jsm>(<jv>fibonacci</jv>, <jv>1</jv>, <jv>1</jv>, <jv>2</jv>, <jv>3</jv>, <jv>5</jv>); 206 * </p> 207 * 208 * @return A {@link Listifier} for {@link Iterable} objects 209 * @see Iterable 210 */ 211 @SuppressWarnings("unchecked") 212 public static Listifier<Iterable> iterableListifier() { 213 return (bc, iterable) -> stream(iterable.spliterator(), false).toList(); 214 } 215 216 /** 217 * Returns a listifier for {@link Iterator} objects that converts them to lists. 218 * 219 * <p><b>Warning:</b> This listifier consumes the iterator during conversion. After listification, 220 * the iterator will be exhausted and cannot be used again.</p> 221 * 222 * <h5 class='section'>Behavior:</h5> 223 * <ul> 224 * <li><b>Element extraction:</b> Consumes all remaining elements from the iterator</li> 225 * <li><b>Order preservation:</b> Maintains the iterator's order in the resulting list</li> 226 * <li><b>Iterator exhaustion:</b> The iterator becomes unusable after conversion</li> 227 * </ul> 228 * 229 * <h5 class='section'>Usage Examples:</h5> 230 * <p class='bjava'> 231 * <jc>// Test with list iterator</jc> 232 * <jk>var</jk> <jv>list</jv> = List.<jsm>of</jsm>(<js>"a"</js>, <js>"b"</js>, <js>"c"</js>); 233 * <jk>var</jk> <jv>iterator</jv> = <jv>list</jv>.iterator(); 234 * <jsm>assertList</jsm>(<jv>iterator</jv>, <js>"a"</js>, <js>"b"</js>, <js>"c"</js>); 235 * <jc>// iterator is now exhausted</jc> 236 * 237 * <jc>// Test with custom iterator</jc> 238 * <jk>var</jk> <jv>numbers</jv> = IntStream.<jsm>range</jsm>(<jv>1</jv>, <jv>4</jv>).iterator(); 239 * <jsm>assertList</jsm>(<jv>numbers</jv>, <jv>1</jv>, <jv>2</jv>, <jv>3</jv>); 240 * </p> 241 * 242 * <h5 class='section'>Important Notes:</h5> 243 * <ul> 244 * <li><b>One-time use:</b> The iterator becomes exhausted and unusable after conversion</li> 245 * <li><b>Side effects:</b> Any side effects in the iterator's next() method will occur</li> 246 * <li><b>Exception handling:</b> Exceptions from the iterator are propagated</li> 247 * </ul> 248 * 249 * @return A {@link Listifier} for {@link Iterator} objects 250 * @see Iterator 251 */ 252 @SuppressWarnings("unchecked") 253 public static Listifier<Iterator> iteratorListifier() { 254 return (bc, iterator) -> stream(spliteratorUnknownSize(iterator, 0), false).toList(); 255 } 256 257 /** 258 * Returns a listifier for {@link Map} objects that converts them to lists of {@link java.util.Map.Entry} objects. 259 * 260 * <p>This listifier enables maps to be processed as lists in BCT assertions, making it easy 261 * to test map contents using list-based assertion methods.</p> 262 * 263 * <h5 class='section'>Behavior:</h5> 264 * <ul> 265 * <li><b>Entry conversion:</b> Each key-value pair becomes a Map.Entry in the list</li> 266 * <li><b>Order preservation:</b> Maintains the map's iteration order</li> 267 * <li><b>Empty maps:</b> Returns empty list for empty maps</li> 268 * <li><b>Map ordering:</b> Converts unordered Maps (HashMap, etc.) to TreeMap for deterministic ordering</li> 269 * </ul> 270 * 271 * <h5 class='section'>Map Ordering Behavior:</h5> 272 * <p>To ensure predictable test results, this listifier handles Maps with unreliable ordering:</p> 273 * <ul> 274 * <li><b>{@link SortedMap} (TreeMap, etc.):</b> Preserves existing sort order</li> 275 * <li><b>{@link LinkedHashMap}:</b> Preserves insertion order</li> 276 * <li><b>{@link HashMap} and other unordered Maps:</b> Converts to {@link TreeMap} for natural key ordering</li> 277 * </ul> 278 * 279 * <h5 class='section'>Usage Examples:</h5> 280 * <p class='bjava'> 281 * <jc>// Test map contents with deterministic ordering</jc> 282 * <jk>var</jk> <jv>map</jv> = Map.<jsm>of</jsm>(<js>"z"</js>, <js>"value1"</js>, <js>"a"</js>, <js>"value2"</js>); 283 * <jsm>assertSize</jsm>(<jv>map</jv>, <jv>2</jv>); 284 * <jc>// Entries will be ordered by key: [a=value2, z=value1]</jc> 285 * 286 * <jc>// LinkedHashMap preserves insertion order</jc> 287 * <jk>var</jk> <jv>linkedMap</jv> = <jk>new</jk> LinkedHashMap<>(); 288 * <jv>linkedMap</jv>.put(<js>"first"</js>, <js>"1"</js>); 289 * <jv>linkedMap</jv>.put(<js>"second"</js>, <js>"2"</js>); 290 * <jc>// Entries will maintain insertion order: [first=1, second=2]</jc> 291 * 292 * <jc>// Test empty map</jc> 293 * <jk>var</jk> <jv>emptyMap</jv> = Map.<jsm>of</jsm>(); 294 * <jsm>assertEmpty</jsm>(<jv>emptyMap</jv>); 295 * 296 * <jc>// Test map in object property</jc> 297 * <jk>var</jk> <jv>config</jv> = <jk>new</jk> Configuration().setProperties(<jv>map</jv>); 298 * <jsm>assertSize</jsm>(<jv>config</jv>, <js>"properties"</js>, <jv>2</jv>); 299 * </p> 300 * 301 * <h5 class='section'>Entry Processing:</h5> 302 * <p>The resulting Map.Entry objects can be further processed by other parts of the 303 * conversion system, typically being stringified to <js>"key=value"</js> format.</p> 304 * 305 * <h5 class='section'>Performance:</h5> 306 * <p>This listifier creates a new ArrayList from the map's entrySet. For unordered Maps, 307 * an additional TreeMap conversion adds O(n log n) sorting overhead based on key ordering. 308 * For large maps, consider the memory implications.</p> 309 * 310 * @return A {@link Listifier} for {@link Map} objects 311 * @see Map 312 * @see java.util.Map.Entry 313 * @see TreeMap 314 * @see LinkedHashMap 315 */ 316 @SuppressWarnings("unchecked") 317 public static Listifier<Map> mapListifier() { 318 return (bc, map) -> { 319 if (! (map instanceof SortedMap) && ! (map instanceof LinkedHashMap) && BctConfiguration.get(BCT_SORT_MAPS, false)) { 320 var map2 = new TreeMap<>(flexibleComparator(bc)); 321 map2.putAll(map); 322 map = map2; 323 } 324 return toList(map.entrySet()); 325 }; 326 } 327 328 /** 329 * Returns a listifier for {@link Stream} objects that converts them to lists. 330 * 331 * <p><b>Warning:</b> This listifier terminates the stream during conversion. After listification, 332 * the stream is closed and cannot be used again.</p> 333 * 334 * <h5 class='section'>Behavior:</h5> 335 * <ul> 336 * <li><b>Stream termination:</b> Calls toList() to collect all stream elements</li> 337 * <li><b>Order preservation:</b> Maintains stream order in the resulting list</li> 338 * <li><b>Stream closure:</b> The stream becomes unusable after conversion</li> 339 * </ul> 340 * 341 * <h5 class='section'>Usage Examples:</h5> 342 * <p class='bjava'> 343 * <jc>// Test with filtered stream</jc> 344 * <jk>var</jk> <jv>numbers</jv> = IntStream.<jsm>range</jsm>(<jv>1</jv>, <jv>10</jv>) 345 * .filter(<jv>n</jv> -> <jv>n</jv> % <jv>2</jv> == <jv>0</jv>) 346 * .boxed(); 347 * <jsm>assertList</jsm>(<jv>numbers</jv>, <jv>2</jv>, <jv>4</jv>, <jv>6</jv>, <jv>8</jv>); 348 * 349 * <jc>// Test with mapped stream</jc> 350 * <jk>var</jk> <jv>words</jv> = Stream.<jsm>of</jsm>(<js>"hello"</js>, <js>"world"</js>) 351 * .map(String::toUpperCase); 352 * <jsm>assertList</jsm>(<jv>words</jv>, <js>"HELLO"</js>, <js>"WORLD"</js>); 353 * </p> 354 * 355 * <h5 class='section'>Important Notes:</h5> 356 * <ul> 357 * <li><b>One-time use:</b> The stream is terminated and cannot be reused</li> 358 * <li><b>Lazy evaluation:</b> Stream operations are executed during listification</li> 359 * <li><b>Exception handling:</b> Stream operation exceptions are propagated</li> 360 * </ul> 361 * 362 * @return A {@link Listifier} for {@link Stream} objects 363 * @see Stream 364 */ 365 @SuppressWarnings("unchecked") 366 public static Listifier<Stream> streamListifier() { 367 return (bc, stream) -> stream.toList(); 368 } 369 370 /** 371 * Creates a comparator that can handle any object type, including non-Comparable objects. 372 * 373 * <p>This comparator provides a flexible comparison strategy that handles various scenarios:</p> 374 * <ul> 375 * <li><b>Null values:</b> Sorted to the beginning (nulls first)</li> 376 * <li><b>Comparable objects:</b> Uses natural ordering via {@link Comparable#compareTo(Object)}</li> 377 * <li><b>Non-Comparable objects:</b> Falls back to string-based comparison using the converter's stringify method</li> 378 * <li><b>Mixed types:</b> Handles comparison between different types gracefully</li> 379 * </ul> 380 * 381 * <p>This comparator is used internally by {@link #collectionListifier()} and {@link #mapListifier()} 382 * to ensure predictable ordering in test assertions, even when dealing with objects that don't 383 * implement {@link Comparable}.</p> 384 * 385 * <h5 class='section'>Comparison Strategy:</h5> 386 * <ol> 387 * <li>If both objects are null, they are equal</li> 388 * <li>If one object is null, it comes first</li> 389 * <li>If both objects are Comparable of compatible types, use natural ordering</li> 390 * <li>Otherwise, stringify both objects and compare the string representations</li> 391 * </ol> 392 * 393 * @param converter The bean converter to use for stringification 394 * @return A comparator that can handle any object type 395 */ 396 private static Comparator<Object> flexibleComparator(BeanConverter converter) { 397 // Use default converter if null is passed 398 BeanConverter conv = converter != null ? converter : BasicBeanConverter.DEFAULT; 399 return (o1, o2) -> { 400 // Try natural ordering using cmp() (handles nulls and Comparable objects) 401 int result = cmp(o1, o2); 402 if (result != 0) 403 return result; 404 405 // Fall back to string comparison 406 var s1 = conv.stringify(o1); 407 var s2 = conv.stringify(o2); 408 return cmp(s1, s2); 409 }; 410 } 411 412 /** 413 * Constructor. 414 */ 415 private Listifiers() {} 416}