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.bct; 018 019import static org.apache.juneau.commons.function.Suppliers.*; 020import static org.apache.juneau.commons.utils.Shorts.*; 021 022import org.apache.juneau.commons.function.*; 023import org.apache.juneau.commons.inject.*; 024import org.apache.juneau.commons.settings.*; 025 026/** 027 * Configuration utility for Bean-Centric Testing (BCT) framework. 028 * 029 * <p> 030 * This class provides thread-local configuration settings for BCT assertions, allowing test-specific 031 * customization of behavior such as map sorting, collection sorting, and bean converter selection. 032 * 033 * <p> 034 * Configuration is managed using thread-local storage, ensuring that parallel test execution doesn't 035 * interfere with each other's settings. Settings are typically configured via the {@link BctConfig @BctConfig} 036 * annotation, but can also be set programmatically. 037 * 038 * <h5 class='section'>Configuration Properties:</h5> 039 * <ul> 040 * <li><b>{@link #BCT_SORT_MAPS}:</b> Enable sorting of maps in assertions</li> 041 * <li><b>{@link #BCT_SORT_COLLECTIONS}:</b> Enable sorting of collections in assertions</li> 042 * <li><b>Bean Converter:</b> Custom bean converter instance for property access and conversion</li> 043 * </ul> 044 * 045 * <h5 class='section'>Usage via Annotation:</h5> 046 * <p class='bjava'> 047 * <ja>@BctConfig</ja>(sortMaps=TriState.<jsf>TRUE</jsf>, beanConverter=MyConverter.<jk>class</jk>) 048 * <jk>class</jk> MyTest { 049 * <ja>@Test</ja> 050 * <ja>@BctConfig</ja>(sortCollections=TriState.<jsf>TRUE</jsf>) 051 * <jk>void</jk> testSomething() { 052 * <jc>// sortMaps=true (from class), sortCollections=true (from method),</jc> 053 * <jc>// beanConverter=MyConverter (from class)</jc> 054 * } 055 * } 056 * </p> 057 * 058 * <h5 class='section'>Usage via Programmatic API:</h5> 059 * <p class='bjava'> 060 * <ja>@BeforeEach</ja> 061 * <jk>void</jk> setUp() { 062 * BctConfiguration.<jsm>set</jsm>(BctConfiguration.<jsf>BCT_SORT_MAPS</jsf>, <jk>true</jk>); 063 * BctConfiguration.<jsm>set</jsm>(<jk>new</jk> MyCustomConverter()); 064 * } 065 * 066 * <ja>@AfterEach</ja> 067 * <jk>void</jk> tearDown() { 068 * BctConfiguration.<jsm>clear</jsm>(); 069 * } 070 * </p> 071 * 072 * <h5 class='section'>Thread Safety:</h5> 073 * <p> 074 * All methods in this class are thread-safe. Configuration is stored in thread-local storage, 075 * ensuring that each test thread has its own isolated configuration that doesn't interfere 076 * with other concurrently running tests. 077 * </p> 078 * 079 * @see BctConfig 080 * @see BctAssertions 081 */ 082@SuppressWarnings({ 083 "java:S1192" // Duplicated literals (argument/property names) read more clearly inline than as constants 084}) 085public class BctConfiguration { 086 087 /** 088 * Private constructor to prevent instantiation. 089 */ 090 private BctConfiguration() { 091 // Utility class - prevent instantiation 092 } 093 094 // Thread-local memoized supplier for default converter (defaults to BasicBeanConverter.DEFAULT) 095 private static final ThreadLocal<Memoizer<BeanConverter>> CONVERTER_SUPPLIER = ThreadLocal.withInitial(() -> memoizer(() -> BasicBeanConverter.DEFAULT)); 096 097 /** 098 * Configuration property name for enabling map sorting in BCT assertions. 099 * 100 * <p> 101 * When set to <jk>true</jk>, maps will be sorted by key before comparison in assertions. 102 * This ensures consistent ordering regardless of the map's internal ordering. 103 * 104 * <h5 class='section'>Example:</h5> 105 * <p class='bjava'> 106 * BctConfiguration.<jsm>set</jsm>(BctConfiguration.<jsf>BCT_SORT_MAPS</jsf>, <jk>true</jk>); 107 * </p> 108 */ 109 public static final String BCT_SORT_MAPS = "Bct.sortMaps"; 110 111 /** 112 * Configuration property name for enabling collection sorting in BCT assertions. 113 * 114 * <p> 115 * When set to <jk>true</jk>, collections will be sorted before comparison in assertions. 116 * This ensures consistent ordering regardless of the collection's internal ordering. 117 * 118 * <h5 class='section'>Example:</h5> 119 * <p class='bjava'> 120 * BctConfiguration.<jsm>set</jsm>(BctConfiguration.<jsf>BCT_SORT_COLLECTIONS</jsf>, <jk>true</jk>); 121 * </p> 122 */ 123 public static final String BCT_SORT_COLLECTIONS = "Bct.sortCollections"; 124 125 // Thread-local override for method-level converter customization 126 private static final ThreadLocal<BeanConverter> CONVERTER_OVERRIDE = new ThreadLocal<>(); 127 128 private static final Settings SETTINGS = Settings.create().build(); 129 130 /** 131 * Sets a thread-local configuration value. 132 * 133 * <p> 134 * The value is stored in thread-local storage and will only affect the current thread. 135 * This is the recommended way to set configuration for individual tests. 136 * 137 * <h5 class='section'>Example:</h5> 138 * <p class='bjava'> 139 * BctConfiguration.<jsm>set</jsm>(BctConfiguration.<jsf>BCT_SORT_MAPS</jsf>, <jk>true</jk>); 140 * </p> 141 * 142 * @param name The configuration property name (e.g., {@link #BCT_SORT_MAPS}). 143 * @param value The value to set. 144 * @see #get(String) 145 * @see #clear() 146 */ 147 public static void set(String name, Object value) { 148 SETTINGS.setLocal(name, s(value)); 149 } 150 151 /** 152 * Sets a global configuration value. 153 * 154 * <p> 155 * Global values apply to all threads and persist until explicitly cleared. 156 * Use with caution in multi-threaded test environments. 157 * 158 * <h5 class='section'>Example:</h5> 159 * <p class='bjava'> 160 * BctConfiguration.<jsm>setGlobal</jsm>(BctConfiguration.<jsf>BCT_SORT_MAPS</jsf>, <jk>true</jk>); 161 * </p> 162 * 163 * @param name The configuration property name (e.g., {@link #BCT_SORT_MAPS}). 164 * @param value The value to set. 165 * @see #get(String) 166 * @see #clearGlobal() 167 */ 168 public static void setGlobal(String name, Object value) { 169 SETTINGS.setGlobal(name, s(value)); 170 } 171 172 /** 173 * Gets a configuration setting by name. 174 * 175 * <p> 176 * Returns the setting object which can be used to check if a value is set and retrieve it. 177 * Thread-local values take precedence over global values. 178 * 179 * @param name The configuration property name (e.g., {@link #BCT_SORT_MAPS}). 180 * @return The setting object, or <jk>null</jk> if not set. 181 * @see #get(String, Object) 182 * @see #set(String, Object) 183 */ 184 public static StringSetting get(String name) { 185 return SETTINGS.get(name); 186 } 187 188 /** 189 * Gets a configuration value by name with a default value. 190 * 191 * <p> 192 * Returns the configured value if set, otherwise returns the provided default value. 193 * Thread-local values take precedence over global values. 194 * 195 * <h5 class='section'>Example:</h5> 196 * <p class='bjava'> 197 * <jk>boolean</jk> <jv>sortMaps</jv> = BctConfiguration.<jsm>get</jsm>(BctConfiguration.<jsf>BCT_SORT_MAPS</jsf>, <jk>false</jk>); 198 * </p> 199 * 200 * @param <T> The type of the value. 201 * @param name The configuration property name (e.g., {@link #BCT_SORT_MAPS}). 202 * @param def The default value to return if not set. 203 * @return The configured value, or the default if not set. 204 * @see #get(String) 205 * @see #set(String, Object) 206 */ 207 public static <T> T get(String name, T def) { 208 return SETTINGS.get(name, def); 209 } 210 211 /** 212 * Gets a boolean configuration value by name with a default value. 213 * 214 * <p> 215 * Returns the configured boolean value if set, otherwise returns the provided default value. 216 * Thread-local values take precedence over global values. 217 * 218 * <h5 class='section'>Example:</h5> 219 * <p class='bjava'> 220 * <jk>boolean</jk> <jv>sortMaps</jv> = BctConfiguration.<jsm>get</jsm>(BctConfiguration.<jsf>BCT_SORT_MAPS</jsf>, <jk>false</jk>); 221 * </p> 222 * 223 * @param name The configuration property name (e.g., {@link #BCT_SORT_MAPS}). 224 * @param def The default boolean value to return if not set. 225 * @return The configured boolean value, or the default if not set. 226 * @see #get(String) 227 * @see #set(String, Object) 228 */ 229 public static boolean get(String name, boolean def) { 230 return SETTINGS.get(name, def); 231 } 232 233 /** 234 * Clears all thread-local configuration settings. 235 * 236 * <p> 237 * This method removes all thread-local settings including: 238 * <ul> 239 * <li>Configuration properties (e.g., {@link #BCT_SORT_MAPS}, {@link #BCT_SORT_COLLECTIONS})</li> 240 * <li>Bean converter override</li> 241 * </ul> 242 * 243 * <p> 244 * This is typically called in test teardown methods (e.g., {@code @AfterEach}) to ensure 245 * a clean state for subsequent tests. The {@link BctConfigExtension BctConfigExtension} 246 * automatically calls this method after each test. 247 * 248 * <h5 class='section'>Example:</h5> 249 * <p class='bjava'> 250 * <ja>@AfterEach</ja> 251 * <jk>void</jk> tearDown() { 252 * BctConfiguration.<jsm>clear</jsm>(); 253 * } 254 * </p> 255 * 256 * @see #clearGlobal() 257 * @see #set(String, Object) 258 */ 259 public static void clear() { 260 SETTINGS.clearLocal(); 261 CONVERTER_OVERRIDE.remove(); 262 CONVERTER_SUPPLIER.remove(); 263 } 264 265 /** 266 * Clears all global configuration settings. 267 * 268 * <p> 269 * This method removes all global settings. Use with caution as this affects all threads. 270 * 271 * @see #clear() 272 * @see #setGlobal(String, Object) 273 */ 274 public static void clearGlobal() { 275 SETTINGS.clearGlobal(); 276 } 277 278 /** 279 * Sets a custom bean converter for the current thread. 280 * 281 * <p> 282 * This method allows you to override the default converter ({@link BasicBeanConverter#DEFAULT}) for all 283 * assertions in the current test method. The converter will be used by all assertion methods in the current thread. 284 * 285 * <p> 286 * This is particularly useful when you need custom property access, stringification, or conversion logic 287 * for specific tests. The converter can be configured via the {@link BctConfig @BctConfig} 288 * annotation or set programmatically. 289 * 290 * <h5 class='section'>Usage Example:</h5> 291 * <p class='bjava'> 292 * <ja>@BeforeEach</ja> 293 * <jk>void</jk> setUp() { 294 * <jk>var</jk> <jv>customConverter</jv> = BasicBeanConverter.<jsm>builder</jsm>() 295 * .defaultSettings() 296 * .addStringifier(LocalDate.<jk>class</jk>, <jp>date</jp> -> <jp>date</jp>.format(DateTimeFormatter.<jsf>ISO_LOCAL_DATE</jsf>)) 297 * .build(); 298 * BctConfiguration.<jsm>set</jsm>(<jv>customConverter</jv>); 299 * } 300 * 301 * <ja>@Test</ja> 302 * <jk>void</jk> testWithCustomConverter() { 303 * <jc>// All assertions use the custom converter</jc> 304 * <jsm>assertBean</jsm>(<jv>event</jv>, <js>"date"</js>, <js>"2023-12-01"</js>); 305 * } 306 * 307 * <ja>@AfterEach</ja> 308 * <jk>void</jk> tearDown() { 309 * BctConfiguration.<jsm>clear</jsm>(); <jc>// Also clears converter override</jc> 310 * } 311 * </p> 312 * 313 * <h5 class='section'>Thread Safety:</h5> 314 * <p> 315 * This method is thread-safe and uses thread-local storage. Each test method running in parallel 316 * will have its own converter instance, preventing cross-thread interference. 317 * </p> 318 * 319 * @param converter The bean converter to use for the current thread. Must not be <jk>null</jk>. 320 * @throws IllegalArgumentException If converter is <jk>null</jk>. 321 * @see #clear() 322 * @see BctConfig#beanConverter() 323 * @see BeanConverter 324 * @see BasicBeanConverter 325 */ 326 public static void set(BeanConverter converter) { 327 reqnn("converter", converter); 328 CONVERTER_OVERRIDE.set(converter); 329 } 330 331 332 /** 333 * Gets the bean converter for the current thread. 334 * 335 * <p>Returns the thread-local converter override if set, otherwise returns the memoized default converter. 336 * This method is used internally by all assertion methods to get the current thread-local converter.</p> 337 * 338 * @return The bean converter to use for the current thread. 339 */ 340 static BeanConverter getConverter() { 341 return o(BctConfiguration.CONVERTER_OVERRIDE.get()).orElseGet(CONVERTER_SUPPLIER.get()); 342 } 343 344 /** 345 * Returns the {@link Value @Value}-resolved defaults for {@link #BCT_SORT_MAPS} and 346 * {@link #BCT_SORT_COLLECTIONS}, sourced from the active 347 * {@link Settings Settings} chain (system properties 348 * → environment variables → registered property sources). Each invocation creates 349 * a fresh, fully-injected instance. 350 * 351 * <p> 352 * This is the canonical seam for "BCT default sort flags" reads — production code that 353 * makes a per-call decision based on the resolved defaults should call 354 * {@code BctConfiguration.defaults().isSortMaps()} / {@code .isSortCollections()} instead of 355 * hand-rolling its own {@code System.getProperty(...)} read. 356 * 357 * @return A new {@link Defaults} instance, populated via {@link BeanInstantiator}. 358 */ 359 public static Defaults defaults() { 360 return BeanInstantiator.of(Defaults.class, BasicBeanStore.INSTANCE).run(); 361 } 362 363 /** 364 * Holder for {@link Value @Value}-resolved BCT default sort flags. 365 * 366 * <p> 367 * Constructed by {@link #defaults()} via {@link BeanInstantiator}; both fields are populated 368 * by {@link Value @Value} annotations whose SVL expressions inline the canonical 369 * {@link #BCT_SORT_MAPS} and {@link #BCT_SORT_COLLECTIONS} property names as string literals 370 * (per the project-wide rule that every {@code @Value} expression is a fully self-contained 371 * string literal — no concatenation with Java constants). The constants remain the 372 * canonical reference for external callers ({@link Listifiers}, {@code BctConfigExtension}, 373 * and the {@code BctConfiguration.set/get} helpers). 374 */ 375 public static class Defaults { 376 377 /** 378 * The {@code Bct.sortMaps} default; {@code true} when the {@link #BCT_SORT_MAPS} property is 379 * set to a truthy value, {@code false} otherwise. 380 */ 381 @Value("${Bct.sortMaps:false}") 382 boolean sortMaps; 383 384 /** 385 * The {@code Bct.sortCollections} default; {@code true} when the {@link #BCT_SORT_COLLECTIONS} 386 * property is set to a truthy value, {@code false} otherwise. 387 */ 388 @Value("${Bct.sortCollections:false}") 389 boolean sortCollections; 390 391 /** Constructor — instantiated by {@link BeanInstantiator} via {@link #defaults()}. */ 392 protected Defaults() {} 393 394 /** 395 * Returns the resolved {@code Bct.sortMaps} default. 396 * 397 * @return {@code true} when the {@link #BCT_SORT_MAPS} property is set to a truthy value, {@code false} otherwise. 398 */ 399 public boolean isSortMaps() { 400 return sortMaps; 401 } 402 403 /** 404 * Returns the resolved {@code Bct.sortCollections} default. 405 * 406 * @return {@code true} when the {@link #BCT_SORT_COLLECTIONS} property is set to a truthy value, {@code false} otherwise. 407 */ 408 public boolean isSortCollections() { 409 return sortCollections; 410 } 411 } 412}