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 java.util.*; 020import java.util.concurrent.*; 021import java.util.function.*; 022 023/** 024 * Collection of standard swapper implementations for the Bean-Centric Testing framework. 025 * 026 * <p>This class provides built-in object transformation strategies that handle common wrapper 027 * types and asynchronous objects. These swappers are automatically registered when using 028 * {@link BasicBeanConverter.Builder#defaultSettings()}.</p> 029 * 030 * <h5 class='section'>Purpose:</h5> 031 * <p>Swappers pre-process objects before stringification or listification, enabling the converter 032 * to handle wrapper types, lazy evaluation objects, and asynchronous results consistently. They 033 * transform objects into more primitive forms that can be easily converted to strings or lists.</p> 034 * 035 * <h5 class='section'>Built-in Swappers:</h5> 036 * <ul> 037 * <li><b>{@link #optionalSwapper()}</b> - Unwraps {@link Optional} values to their contained objects or <jk>null</jk></li> 038 * <li><b>{@link #supplierSwapper()}</b> - Evaluates {@link Supplier} functions to get their supplied values</li> 039 * <li><b>{@link #futureSwapper()}</b> - Extracts completed {@link Future} results or returns status messages</li> 040 * </ul> 041 * 042 * <h5 class='section'>Usage Example:</h5> 043 * <p class='bjava'> 044 * <jc>// Register swappers using builder</jc> 045 * <jk>var</jk> <jv>converter</jv> = BasicBeanConverter.<jsm>builder</jsm>() 046 * .defaultSettings() 047 * .addSwapper(Optional.<jk>class</jk>, Swappers.<jsm>optionalSwapper</jsm>()) 048 * .addSwapper(Supplier.<jk>class</jk>, Swappers.<jsm>supplierSwapper</jsm>()) 049 * .addSwapper(Future.<jk>class</jk>, Swappers.<jsm>futureSwapper</jsm>()) 050 * .build(); 051 * </p> 052 * 053 * <h5 class='section'>Custom Swapper Development:</h5> 054 * <p>When creating custom swappers, follow these patterns:</p> 055 * <ul> 056 * <li><b>Null Safety:</b> Handle <jk>null</jk> inputs gracefully</li> 057 * <li><b>Exception Handling:</b> Convert exceptions to meaningful error messages</li> 058 * <li><b>Status Indication:</b> Provide clear status for incomplete or error states</li> 059 * <li><b>Non-blocking:</b> Avoid blocking operations that might hang tests</li> 060 * <li><b>Recursion Prevention:</b> Ensure swappers don't create circular transformations that could lead to {@link StackOverflowError}. 061 * The framework does not check for recursion, so developers must avoid registering swappers that transform objects 062 * back to types that would trigger the same or other swappers in an endless cycle.</li> 063 * </ul> 064 * 065 * @see Swapper 066 * @see BasicBeanConverter.Builder#addSwapper(Class, Swapper) 067 * @see BasicBeanConverter.Builder#defaultSettings() 068 */ 069@SuppressWarnings("rawtypes") 070public class Swappers { 071 072 /** 073 * Constructor. 074 */ 075 private Swappers() {} 076 077 /** 078 * Returns a swapper for {@link Optional} objects that unwraps them to their contained values. 079 * 080 * <p>This swapper extracts the value from Optional instances, returning the contained object 081 * if present, or <jk>null</jk> if the Optional is empty. This allows Optional-wrapped values 082 * to be processed naturally by the converter without special handling.</p> 083 * 084 * <h5 class='section'>Behavior:</h5> 085 * <ul> 086 * <li><b>Present value:</b> Returns the contained object for further processing</li> 087 * <li><b>Empty Optional:</b> Returns <jk>null</jk>, which will be rendered as the configured null value</li> 088 * </ul> 089 * 090 * <h5 class='section'>Usage Examples:</h5> 091 * <p class='bjava'> 092 * <jc>// Test Optional with value</jc> 093 * <jk>var</jk> <jv>optional</jv> = Optional.<jsm>of</jsm>(<js>"Hello"</js>); 094 * <jsm>assertBean</jsm>(<jv>optional</jv>, <js>"<self>"</js>, <js>"Hello"</js>); 095 * 096 * <jc>// Test empty Optional</jc> 097 * <jk>var</jk> <jv>empty</jv> = Optional.<jsm>empty</jsm>(); 098 * <jsm>assertBean</jsm>(<jv>empty</jv>, <js>"<self>"</js>, <js>"<null>"</js>); 099 * 100 * <jc>// Test nested Optional in object</jc> 101 * <jk>var</jk> <jv>user</jv> = <jk>new</jk> User().setName(Optional.<jsm>of</jsm>(<js>"John"</js>)); 102 * <jsm>assertBean</jsm>(<jv>user</jv>, <js>"name"</js>, <js>"John"</js>); 103 * </p> 104 * 105 * <h5 class='section'>Integration:</h5> 106 * <p>This swapper is particularly useful when working with modern Java APIs that return 107 * Optional values. It allows test assertions to focus on the actual data rather than 108 * Optional wrapper mechanics.</p> 109 * 110 * @return A {@link Swapper} for {@link Optional} objects 111 * @see Optional 112 */ 113 public static Swapper<Optional> optionalSwapper() { 114 return (bc, optional) -> optional.orElse(null); 115 } 116 117 /** 118 * Returns a swapper for {@link Supplier} objects that evaluates them to get their supplied values. 119 * 120 * <p>This swapper calls the {@link Supplier#get()} method to obtain the value that the supplier 121 * provides. This enables testing of lazy-evaluated or dynamically-computed values as if they 122 * were regular objects.</p> 123 * 124 * <h5 class='section'>Behavior:</h5> 125 * <ul> 126 * <li><b>Successful evaluation:</b> Returns the result of calling {@link Supplier#get()}</li> 127 * <li><b>Exception during evaluation:</b> Allows the exception to propagate (no special handling)</li> 128 * </ul> 129 * 130 * <h5 class='section'>Usage Examples:</h5> 131 * <p class='bjava'> 132 * <jc>// Test supplier with simple value</jc> 133 * <jk>var</jk> <jv>supplier</jv> = () -> <js>"Hello World"</js>; 134 * <jsm>assertBean</jsm>(<jv>supplier</jv>, <js>"<self>"</js>, <js>"Hello World"</js>); 135 * 136 * <jc>// Test supplier in object property</jc> 137 * <jk>var</jk> <jv>config</jv> = <jk>new</jk> Configuration().setDynamicValue(() -> calculateValue()); 138 * <jsm>assertBean</jsm>(<jv>config</jv>, <js>"dynamicValue"</js>, <js>"computed-result"</js>); 139 * 140 * <jc>// Test supplier returning null</jc> 141 * <jk>var</jk> <jv>nullSupplier</jv> = () -> <jk>null</jk>; 142 * <jsm>assertBean</jsm>(<jv>nullSupplier</jv>, <js>"<self>"</js>, <js>"<null>"</js>); 143 * </p> 144 * 145 * <h5 class='section'>Important Notes:</h5> 146 * <ul> 147 * <li><b>Side Effects:</b> The supplier will be evaluated during conversion, which may cause side effects</li> 148 * <li><b>Performance:</b> Expensive computations in suppliers will impact test performance</li> 149 * <li><b>Exception Handling:</b> Exceptions from suppliers are not caught by this swapper</li> 150 * </ul> 151 * 152 * @return A {@link Swapper} for {@link Supplier} objects 153 * @see Supplier 154 */ 155 public static Swapper<Supplier> supplierSwapper() { 156 return (bc, supplier) -> supplier.get(); 157 } 158 159 /** 160 * Returns a swapper for {@link Future} objects that extracts completed results or status information. 161 * 162 * <p>This swapper handles {@link Future} objects in a non-blocking manner, providing meaningful 163 * information about the future's state without waiting for completion:</p> 164 * 165 * <h5 class='section'>Behavior:</h5> 166 * <ul> 167 * <li><b>Completed successfully:</b> Returns the actual result value</li> 168 * <li><b>Completed with exception:</b> Returns <js>"<error: {message}>"</js> format</li> 169 * <li><b>Cancelled:</b> Returns <js>"<cancelled>"</js></li> 170 * <li><b>Still pending:</b> Returns <js>"<pending>"</js></li> 171 * </ul> 172 * 173 * <h5 class='section'>Usage Examples:</h5> 174 * <p class='bjava'> 175 * <jc>// Test completed future</jc> 176 * <jk>var</jk> <jv>future</jv> = CompletableFuture.<jsm>completedFuture</jsm>(<js>"Hello"</js>); 177 * <jsm>assertBean</jsm>(<jv>future</jv>, <js>"<self>"</js>, <js>"Hello"</js>); 178 * 179 * <jc>// Test pending future</jc> 180 * <jk>var</jk> <jv>pending</jv> = <jk>new</jk> CompletableFuture<String>(); 181 * <jsm>assertBean</jsm>(<jv>pending</jv>, <js>"<self>"</js>, <js>"<pending>"</js>); 182 * 183 * <jc>// Test cancelled future</jc> 184 * <jk>var</jk> <jv>cancelled</jv> = <jk>new</jk> CompletableFuture<String>(); 185 * <jv>cancelled</jv>.cancel(<jk>true</jk>); 186 * <jsm>assertBean</jsm>(<jv>cancelled</jv>, <js>"<self>"</js>, <js>"<cancelled>"</js>); 187 * 188 * <jc>// Test failed future</jc> 189 * <jk>var</jk> <jv>failed</jv> = <jk>new</jk> CompletableFuture<String>(); 190 * <jv>failed</jv>.completeExceptionally(<jk>new</jk> RuntimeException(<js>"Test error"</js>)); 191 * <jsm>assertBean</jsm>(<jv>failed</jv>, <js>"<self>"</js>, <js>"<error: Test error>"</js>); 192 * </p> 193 * 194 * <h5 class='section'>Non-blocking Guarantee:</h5> 195 * <p>This swapper never calls {@link Future#get()} without first checking {@link Future#isDone()}, 196 * ensuring that test execution is never blocked by incomplete futures. This makes it safe to use 197 * in unit tests without risking hangs or timeouts.</p> 198 * 199 * <h5 class='section'>Error Handling:</h5> 200 * <p>When a future completes exceptionally, the swapper extracts the exception message and 201 * formats it as <js>"<error: {message}>"</js>. This provides useful debugging information 202 * while maintaining a consistent string format for assertions.</p> 203 * 204 * @return A {@link Swapper} for {@link Future} objects 205 * @see Future 206 * @see CompletableFuture 207 */ 208 public static Swapper<Future> futureSwapper() { 209 return (bc, future) -> { 210 if (future.isDone() && !future.isCancelled()) { 211 try { 212 return future.get(); 213 } catch (Exception e) { // NOSONAR 214 return "<error: " + e.getMessage() + ">"; 215 } 216 } 217 return future.isCancelled() ? "<cancelled>" : "<pending>"; 218 }; 219 } 220}