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    * &rarr; environment variables &rarr; registered property sources). Each invocation creates
349    * a fresh, fully-injected instance.
350    *
351    * <p>
352    * This is the canonical seam for &quot;BCT default sort flags&quot; 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 &mdash; 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 &mdash; 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}