This document outlines the rules, guidelines, and best practices that AI assistants follow when working on the Apache Juneau project.
Note: This file is referred to as “my rules” and serves as the definitive reference for all guidelines and conventions I follow when working on the Apache Juneau project.
Important: Also read AI_SESSION.md to understand the current session‘s work context, what we’re currently working on, and any recent changes or patterns established in this session.
Documentation Separation Rule: AI_SESSION.md should only contain session-specific information that is not already covered in AI.md. General rules, permanent conventions, and best practices belong in AI.md. Session-specific work progress, current tasks, and temporary patterns belong in AI_SESSION.md.
@SuppressWarnings annotations to the appropriate class or method level. Use the specific rule ID from the warning (e.g., java:S100, java:S115, java:S116). Apply class-level suppressions when multiple methods/fields are affected, or method-level suppressions for specific methods.@SuppressWarnings annotations, use this format:@SuppressWarnings({ "java:Sxxx" // Short reason why })Use the multi-line format with curly braces and include a brief comment explaining why the suppression is needed.
scripts/start-docusaurus.pyscripts/revert-staged.pyscripts/revert-unstaged.pyscripts/start-examples-rest-jetty.pyscripts/start-examples-springboot.py./scripts/push.py "Commit message" to push changes. Always prompt the user for the commit message and provide 2-3 suggested commit message options based on the changes made. Example: ./scripts/push.py "Replace Stream.collect(Collectors.toList()) with Stream.toList()"scripts/build-and-test.pyAI.md (permanent/general)AI_SESSION.md (session-specific)AI.md (permanent/general)When adding or modifying Java code, ALWAYS preserve the exact indentation of the surrounding code:
Critical Requirements:
old_stringCommon Patterns:
<TAB>/** <TAB> * Javadoc line 1 <TAB> * Javadoc line 2 <TAB> */ <TAB>@Annotation <TAB>public ReturnType methodName() { <TAB><TAB>return statement; <TAB>}
Where <TAB> represents an actual tab character (\t), not spaces.
Why This Matters:
Verification Steps:
new_string, examine the indentation in old_stringExample Correction:
// WRONG - Missing leading tab /** * Method description */ public void method() { // CORRECT - Includes leading tab <TAB>/** <TAB> * Method description <TAB> */ <TAB>public void method() {
When throwing exceptions in Java code:
Use ThrowableUtils methods instead of direct exception constructors:
illegalArg(String message, Object... args) for IllegalArgumentExceptionruntimeException(String message, Object... args) for generic RuntimeExceptionimport static org.apache.juneau.common.utils.ThrowableUtils.*;Use MessageFormat-style placeholders with ThrowableUtils methods:
{0}, {1}, etc. for parameter placeholders'' (e.g., "Value ''{0}'' is invalid")Examples:
// WRONG - Direct exception constructor throw new IllegalArgumentException("Value '" + value + "' is invalid"); // CORRECT - ThrowableUtils with MessageFormat throw illegalArg("Value ''{0}'' is invalid", value); // WRONG - String concatenation throw new RuntimeException("Failed to process " + name + " with id " + id); // CORRECT - ThrowableUtils with multiple arguments throw runtimeException("Failed to process {0} with id {1}", name, id);
When declaring local variables in Java code:
Use the var keyword whenever the type is obvious from the right-hand side:
var map = new HashMap<String,Integer>();var list = getList();var result = stream.collect(toList());for (var entry : map.entrySet())Keep explicit types when:
InputStream stream = getStream();boolean hasNext = iterator.hasNext(); (better than var hasNext)List<String> list = new ArrayList<>();Examples:
// WRONG - Redundant type declaration Map<String,Integer> map = new HashMap<String,Integer>(); List<String> keys = map.keySet(); // CORRECT - Use var var map = new HashMap<String,Integer>(); var keys = map.keySet();
When using instanceof pattern matching (Java 16+), follow this naming convention:
General Rule: Append “2” to the original variable name:
if (o instanceof Type o2) - original variable is o, pattern variable is o2if (value instanceof Calendar value2) - original variable is value, pattern variable is value2if (this instanceof MethodInfo this2) - original variable is this, pattern variable is this2Exception for variables ending in “2”: When the original variable already ends in “2” (like o2), use “o3” instead of “o22” for simplicity:
if (o2 instanceof InputStream o3) - original variable is o2, pattern variable is o3if (o1 instanceof Comparable o3) - original variable is o1, pattern variable is o3 (not o12)Conflict Resolution: When there's a naming conflict (e.g., a lambda parameter with the same name), use a different name:
if (value instanceof BeanMap<?> value2) followed by value2.forEachValue(..., (pMeta2, key2, value3, thrown2) -> ...) - lambda parameter uses value3 to avoid conflict with pattern variable value2Examples:
// CORRECT - Standard pattern if (o instanceof BeanMap o2) serializeBeanMap(out, o2, typeName); // CORRECT - Variable ending in "2" for (var o2 : o) { if (o2 instanceof InputStream o3) o3.close(); } // CORRECT - Conflict resolution if (value instanceof BeanMap<?> value2) { value2.forEachValue(x -> true, (pMeta2, key2, value3, thrown2) -> { // value3 used to avoid conflict with value2 }); } // WRONG - Not following convention if (o instanceof BeanMap bm) // Should be o2 if (value instanceof Calendar cal) // Should be value2
When declaring class fields, always use final to ensure true immutability:
Always use final for fields:
final whenever possibleUse find helper methods for memoized fields:
Supplier fields need constructor parameters, use helper methodsfind prefix (e.g., findGenericInterfaces())Why this matters:
Examples:
// WRONG - Non-final field with comment Class<?> c; // Effectively final // CORRECT - Final field with helper method pattern public class ClassInfo { private final Class<?> c; // Final supplier initialized using helper method that accesses constructor parameter private final Supplier<List<Type>> genericInterfacesCache = memoize(this::findGenericInterfaces); public ClassInfo(Class<?> c) { this.c = c; } // Helper method called during field initialization private List<Type> findGenericInterfaces() { return c == null ? Collections.emptyList() : u(l(c.getGenericInterfaces())); } }
Pattern Summary:
final fieldsfind helper methods that use those fieldsSupplier fields with method references to the helper methodsfinal while still supporting lazy initializationWhen working with git operations, especially reverting changes:
Use helper scripts for reverting - Always use the provided Python scripts instead of git commands directly:
./scripts/revert-unstaged.py path/to/specific/File.java (reverts unstaged to staged)./scripts/revert-staged.py path/to/specific/File.java (reverts to HEAD, discards all changes)git restore, git checkout, or any git commands directlyRevert Unstaged Changes Script - ./scripts/revert-unstaged.py
git restore --source=INDEX <file>Revert Staged Changes Script - ./scripts/revert-staged.py
git restore --source=HEAD <file>Always revert one file at a time - Never use broad wildcards or multiple file paths:
./scripts/revert-unstaged.py path/to/specific/File.javaWhy this matters:
Proper workflow when compilation fails:
./scripts/revert-unstaged.py path/to/ProblematicFile.javaExamples:
# CORRECT - Revert unstaged changes only (preserves staged) ./scripts/revert-unstaged.py juneau-core/juneau-marshall/src/main/java/org/apache/juneau/objecttools/ObjectSearcher.java # CORRECT - Revert all changes back to HEAD (discards everything) ./scripts/revert-staged.py juneau-core/juneau-marshall/src/main/java/org/apache/juneau/objecttools/ObjectSearcher.java # WRONG - Don't use git commands directly git restore --source=INDEX path/to/file git checkout -- path/to/file
Understanding the scripts:
revert-unstaged.py: Uses git restore --source=INDEX to revert to staged versionrevert-staged.py: Uses git restore --source=HEAD to revert to last commitassertBean()IMPORTANT: Maven vs IDE Compilation Divergence
When compiling code using Maven and the user compiles through their IDE (Eclipse), the compiled code can diverge, leading to unexpected behavior such as:
Eclipse “Build Automatically” Setting: The user typically has “Refresh using native hooks or polling” enabled in Eclipse, and may or may not have “Build Automatically” enabled.
If code changes don't appear to be taking effect or are getting overwritten:
Resolution Strategy: If you encounter situations where your code changes don't appear to be taking effect:
mvn clean install to clear all compiled artifacts and rebuild freshWhen to suspect this issue:
Java Runtime Location: If you can't find Java on the file system using standard commands, look for it in the ~/jdk folder. For example:
~/jdk/openjdk_17.0.14.0.101_17.57.18_aarch64/bin/javaBuild and Test Script: A reusable Python script is available at scripts/build-and-test.py for common Maven operations.
Usage:
# Default: Clean build + run tests ./scripts/build-and-test.py # Build only (skip tests) ./scripts/build-and-test.py --build-only ./scripts/build-and-test.py -b # Test only (no build) ./scripts/build-and-test.py --test-only ./scripts/build-and-test.py -t # Full build and test (explicit) ./scripts/build-and-test.py --full ./scripts/build-and-test.py -f # Verbose output (show full Maven output) ./scripts/build-and-test.py --verbose ./scripts/build-and-test.py -v # Help ./scripts/build-and-test.py --help ./scripts/build-and-test.py -h
What it does:
--build-only/-b: Runs mvn clean install -q -DskipTests--test-only/-t: Runs mvn test -q -Drat.skip=true--full/-f (default): Runs both build and test in sequence--verbose/-v to see full Maven outputWhen to use:
Adding Timeouts to Commands:
When running terminal commands that use head, tail, or pipe operations that might hang waiting for input, always wrap them with the timeout command to prevent indefinite hangs:
Use timeout for commands with head/tail:
command | grep pattern | head -5timeout 30s sh -c 'command | grep pattern | head -5'Timeout durations:
Why this matters:
head/tail can hang indefinitely if the input stream doesn't closeExamples:
# Quick command with timeout timeout 10s sh -c 'mvn test-compile 2>&1 | grep "ERROR" | head -5' # Maven command with longer timeout timeout 120s sh -c 'mvn clean install 2>&1 | tail -20'
assertThrowsWithMessage for exception testing// HTT (Hard To Test) on that line to document why it's difficult to testhttps://juneau.apache.org/docs/topics/{@link} tagsTODO.md file in the project rootTODO.md file using the write or search_replace toolsTODO.md file/juneau-docs directoryRELEASE-NOTES.txt file/juneau-docs/9.2.0.mdWhen adding fluent setter overrides to classes:
@Override /* Overridden from ParentClass */ public ChildClass setProperty1(Type value) { super.setProperty1(value); return this; } @Override /* Overridden from ParentClass */ public ChildClass setProperty2(Type value) { super.setProperty2(value); return this; }
When creating a properties() method that uses the fluent map API with .a() method calls:
Utils.filteredBeanPropertyMap(): Use filteredBeanPropertyMap() to create the map. This method provides sorted, filtered, fluent maps specifically designed for bean property maps.// @formatter:off and // @formatter:on comments to preserve formattingprotected FluentMap<String,Object> properties() { // @formatter:off return filteredBeanPropertyMap() .a("property1", value1) .a("property2", value2) .a("property3", value3); // @formatter:on }
Rationale:
filteredBeanPropertyMap() automatically provides sorted entries, filtered values (removes empty/null values), and a fluent APIThis document outlines the documentation conventions, formatting rules, and best practices for the Apache Juneau project.
Java Variables in Examples:
<jv> tags<jv>json</jv>, <jv>swagger</jv>, <jv>result</jv>Static Method References:
<jsm> tags for static method namesJson.<jsm>from</jsm>(<jv>x</jv>)Static Field References:
<jsf> tags for static field namesJsonSerializer.<jsf>DEFAULT</jsf>Code Comments:
<jc> tags for code comments<jc>// Serialize using JsonSerializer.</jc>Class References:
<jk> tags for class names in text<jk>null</jk>, <jk>String</jk>Java Code Tags:
<jc> - Java comment (green)<jd> - Javadoc comment (blue)<jt> - Javadoc tag (blue, bold)<jk> - Java keyword (purple, bold)<js> - Java string (blue)<jf> - Java field (dark blue)<jsf> - Java static field (dark blue, italic)<jsm> - Java static method (italic)<ja> - Java annotation (grey)<jp> - Java parameter (brown)<jv> - Java local variable (brown)XML Code Tags:
<xt> - XML tag (dark cyan)<xa> - XML attribute (purple)<xc> - XML comment (medium blue)<xs> - XML string (blue, italic)<xv> - XML value (black)JSON Code Tags:
<joc> - JSON comment (green)<jok> - JSON key (purple)<jov> - JSON value (blue)URL Encoding/UON Tags:
<ua> - Attribute name (black)<uk> - true/false/null (purple, bold)<un> - Number value (dark blue)<us> - String value (blue)Manifest File Tags:
<mc> - Manifest comment (green)<mk> - Manifest key (dark red, bold)<mv> - Manifest value (dark blue)<mi> - Manifest import (dark blue, italic)Config File Tags:
<cc> - Config comment (green)<cs> - Config section (dark red, bold)<ck> - Config key (dark red)<cv> - Config value (dark blue)<ci> - Config import (dark red, bold, italic)Special Tags:
<c> - Synonym for <code><dc> - Deleted code (strikethrough)<bc> - Bold code (bold)Code Block Classes:
bcode - Bordered code blockbjava - Bordered Java code blockbjson - Bordered JSON code blockbxml - Bordered XML code blockbini - Bordered INI code blockbuon - Bordered UON code blockburlenc - Bordered URL encoding code blockbconsole - Bordered console output (black background, yellow text)bschema - Bordered schema code blockcode - Unbordered code blockStandard Method Javadoc:
/** * Brief description of what the method does. * * <p> * Longer description if needed, explaining the purpose and behavior. * </p> * * @param paramName Description of the parameter. * @return Description of what is returned. * @throws ExceptionType Description of when this exception is thrown. */
Property Documentation:
/** * The property name. * * @param value The new value for this property. * @return This object. */
Standard Parameters:
value as the parameter name for fluent settersBuilder Method Parameter Naming:
value as the parameter namethis. modifier (e.g., field = value instead of this.field = value)Null Parameter Handling:
/** * Sets the property value. * * @param value The new value. Can be <jk>null</jk> to unset the property. * @return This object. */
Required Parameters:
/** * Sets the property value. * * @param value The new value. Must not be <jk>null</jk>. * @return This object. */
Juneau Documentation Site:
https://juneau.apache.org/docs/topics/Examples:
/** * <h5 class='section'>See Also:</h5><ul> * <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/ModuleName">module-name</a> * </ul> */
External Specification Links:
/** * <a class="doclink" href="https://example.com/specification/#property">property</a> description. */
Internal Class References:
{@link ClassName} for internal class references{@link ClassName#methodName} for method referencesExternal References:
<a class="doclink" href="URL">text</a> for external linksStandard Pattern:
/** * <jc>// Serialize using JsonSerializer.</jc> * String <jv>json</jv> = Json.<jsm>from</jsm>(<jv>x</jv>); * * <jc>// Or just use toString() which does the same as above.</jc> * String <jv>json</jv> = <jv>x</jv>.toString(); */
Consistency Requirements:
Json.from() and toString() examples<jv>json</jv>, <jv>x</jv>)Fluent Setter Examples:
/** * <jc>// Create a link element</jc> * A <jv>link</jv> = a().href("https://example.com").target("_blank"); */
Builder Pattern Examples:
/** * <jc>// Create a Swagger document</jc> * Swagger <jv>swagger</jv> = swagger() * .info(info().title("My API").version("1.0")) * .path("/users", pathItem().get(operation().summary("Get users"))); */
Standard Property Documentation:
/** * The property description. * * @param value The new value for this property. * @return This object. */
Property with External Link Documentation:
/** * The <a class="doclink" href="https://example.com/spec#property">property</a> description. * * @param value A description of the parameter. * @return This object. */
Standard Structure:
/** * Brief description of the class purpose. * * <h5 class='section'>See Also:</h5><ul> * <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/ModuleName">module-name</a> * </ul> */
Builder Classes:
/** * Builder class for creating {@link ClassName} objects. * * <p> * This class provides fluent methods for building complex objects. * </p> */
Null Parameter Validation:
/** * Sets the property value. * * @param value The new value. Must not be <jk>null</jk>. * @throws IllegalArgumentException If value is <jk>null</jk>. * @return This object. */
String Validation:
/** * Sets the property value. * * @param value The new value. Must not be <jk>null</jk> or blank. * @throws IllegalArgumentException If value is <jk>null</jk> or blank. * @return This object. */
Strict Mode Behavior:
/** * Sets the property value. * * <p> * In strict mode, invalid values will throw {@link RuntimeException}. * In non-strict mode, invalid values are ignored. * </p> * * @param value The new value. * @return This object. */
Standard Constructor:
/** * Constructor. * * @param children The child nodes. */ public ClassName(Object...children) {
Parameterized Constructor:
/** * Constructor. * * @param param1 Description of first parameter. * @param param2 Description of second parameter. */ public ClassName(Type1 param1, Type2 param2) {
Method Overrides:
@Override /* <SimpleClassName> */ public ClassName methodName(Type value) {
Interface Implementations:
@Override /* <SimpleClassName> */ public ClassName methodName(Type value) {
value for fluent setters)/** * Sets the property value. * * @param value The new value for this property. * @return This object. */ public ClassName property(Type value) {
/** * Sets the collection of items. * * @param value The new collection. Can be <jk>null</jk> to unset the property. * @return This object. */ public ClassName items(Collection<Item> value) {
/** * Creates a new instance. * * @return A new instance. */ public static ClassName create() {
This document serves as the definitive guide for documentation in the Apache Juneau project, ensuring consistency, completeness, and clarity across all documentation.
This document outlines the testing conventions, methodologies, and best practices for unit testing in the Apache Juneau project.
Purpose: Provides consistent, readable naming for test data that improves test maintainability and readability.
Rules:
a, b, c, etc.)a1, a2, b1, b2, etc.)Examples:
// Single values var a = swagger(); var b = info(); // Multiple values on same line var a1 = operation("get", "/users"); var a2 = operation("post", "/users"); var b1 = response("200"); var b2 = response("400"); // New bean instance - reset to 'a' var a = new Swagger(); var b = a.info();
Purpose: Assert on actual nested property values using deep property paths rather than just collection sizes.
Usage: Use assertBean() with deep property paths to verify actual values, not just collection sizes.
Examples:
// Instead of just checking size assertNotNull(swagger.getPaths()); assertEquals(2, swagger.getPaths().size()); // Use deep property path assertions assertBean(swagger, "paths{get:/users{summary,operationId},post:/users{summary,operationId}}");
Purpose: Use TestUtils methods instead of direct serializer/parser calls for consistency and readability.
Methods:
TestUtils.json(Object) - Serialize object to JSON stringTestUtils.json(String, Class) - Deserialize JSON string to objectTestUtils.jsonRoundTrip(Object, Class) - Round-trip serialization/deserialization testingExamples:
// Instead of direct serializer calls String json = Json5Serializer.DEFAULT.toString(swagger); Swagger parsed = Json5Parser.DEFAULT.parse(json, Swagger.class); // Use TestUtils convenience methods String json = json(swagger); Swagger parsed = json(json, Swagger.class); Swagger roundTrip = jsonRoundTrip(swagger, Swagger.class);
All test methods follow the pattern: LNN_testName where:
Examples:
a01_basicTest() - First test in category ‘a’b05_serializationTest() - Fifth test in category ‘b’c12_validationTest() - Twelfth test in category ‘c’For test classes with a small number of tests:
public class BeanName_Test extends TestBase { @Test void a01_basicPropertyTest() { // Test basic properties } @Test void a02_fluentSetters() { // Test fluent setter methods } @Test void b01_serialization() { // Test JSON serialization } @Test void b02_deserialization() { // Test JSON deserialization } }
Key Points:
For test classes with large numbers of tests that can be grouped into major categories:
public class BeanName_Test extends TestBase { @Nested class A_basicTests extends TestBase { @Test void a01_properties() { // Test bean properties using varargs setters // Use SSLLC naming convention // Use DPPAP for assertions } @Test void a02_fluentSetters() { // Test fluent setter chaining } } @Nested class B_serialization extends TestBase { @Test void b01_toJson() { // Test JSON serialization // Use TestUtils convenience methods } @Test void b02_fromJson() { // Test JSON deserialization } } @Nested class C_extraProperties extends TestBase { @Test void c01_dynamicProperties() { // Test set(String, Object) method // Use set(String, Object) for ALL the same properties as A_basicTests // Match values from A_basicTests exactly } } @Nested class D_additionalMethods extends TestBase { @Test void d01_collectionSetters() { // Test setX(Collection<X>) methods } @Test void d02_varargAdders() { // Test addX(X...) methods } } }
Key Points:
@Nested inner classes for major test categoriesL_categoryName where L is an uppercase letter (A, B, C, etc.)l##_testName pattern where l is the lowercase letter matching the nested class (a, b, c, etc.) and ## is an incrementing number (01, 02, 03, etc.)TestBase to inherit test utilities and setupWhile not prescriptive, common test category prefixes include:
Choose the structure (Simple vs Complex) based on:
Basic Usage:
assertBean(bean, "property1,property2,property3");
Deep Property Paths:
assertBean(bean, "paths{get:/users{summary,operationId}}");
Collection Assertions:
// Use # notation for uniform collections assertBean(bean, "parameters{#{in,name}}"); // Use explicit indexing for non-uniform collections assertBean(bean, "parameters{0{in,name},1{in,name}}");
Map Assertions:
// Use assertMap for map entries assertMap(map, "key1=value1", "key2=value2");
Use assertThrowsWithMessage:
assertThrowsWithMessage(IllegalArgumentException.class, "Parameter 'name' cannot be null", () -> bean.setName(null));
Property Coverage: Ensure A_basicTests covers all bean properties and C_extraProperties covers the same properties using the set() method.
Getter/Setter Variants: Test both collection variants and varargs variants where applicable.
Parameter Naming: All fluent setters should use value as the parameter name for consistency.
Method Chaining: Test fluent setter chaining:
var result = bean .property1("value1") .property2("value2") .property3("value3");
Rule: All collection bean properties should have exactly 4 methods:
setX(X...) - varargs settersetX(Collection<X>) - Collection setteraddX(X...) - varargs adderaddX(Collection<X>) - Collection adderExamples:
// For a tags property of type Set<String>: public Bean setTags(String...value) { ... } public Bean setTags(Collection<String> value) { ... } public Bean addTags(String...values) { ... } public Bean addTags(Collection<String> values) { ... }
Rule: When a bean has both varargs and Collection setter methods for the same property:
setTags("tag1", "tag2"))setTags(list("tag1", "tag2")))Examples:
// A_basicTests - use varargs .setTags("tag1", "tag2") .setConsumes(MediaType.of("application/json")) // D_additionalMethods - test Collection version .setTags(list("tag1", "tag2")) .setConsumes(list(MediaType.of("application/json"), MediaType.of("application/xml")))
D_additionalMethods Test Structure: This test class should contain three tests:
d01_collectionSetters: Tests setX(Collection<X>) methods
@Test void d01_collectionSetters() { var x = bean() .setTags(list("tag1", "tag2")) .setConsumes(list(MediaType.of("application/json"), MediaType.of("application/xml"))); assertBean(x, "tags,consumes", "[tag1,tag2],[application/json,application/xml]" ); }
d02_varargAdders: Tests addX(X...) methods - each method should be called twice
@Test void d02_varargAdders() { var x = bean() .addTags("tag1") .addTags("tag2") .addConsumes(MediaType.of("application/json")) .addConsumes(MediaType.of("application/xml")); assertBean(x, "tags,consumes", "[tag1,tag2],[application/json,application/xml]" ); }
d03_collectionAdders: Tests addX(Collection<X>) methods - each method should be called twice
@Test void d03_collectionAdders() { // Note: Collection versions of addX methods exist but are difficult to test // due to Java method resolution preferring varargs over Collection // For now, we test the basic functionality with varargs versions var x = bean(); // Test that the addX methods work by calling them multiple times x.addTags("tag1"); x.addTags("tag2"); x.addConsumes(MediaType.of("application/json")); x.addConsumes(MediaType.of("application/xml")); assertBean(x, "tags,consumes", "[tag1,tag2],[application/json,application/xml]" ); }
In all cases, assertBean should be used to validate results.
assertBean() over individual property assertionsRule: For test helper classes that are only used by one test, prefix the class name with the test method prefix and move it immediately above the test that uses it.
Naming Pattern: LNN_ClassName where:
Placement: Move the helper class definition to immediately above the test method that uses it.
Examples:
// WRONG - Generic name, defined far from usage public static class BeanWithDeprecatedGetInstance { // ... } @Test void c03_createBeanIgnoresDeprecatedGetInstance() { var bean = bc(BeanWithDeprecatedGetInstance.class).run(); // ... } // CORRECT - Prefixed with test name, placed above test @Test void c03_createBeanIgnoresDeprecatedGetInstance() { // Bean with both deprecated and valid getInstance methods public static class C03_BeanWithDeprecatedGetInstance { private static final C03_BeanWithDeprecatedGetInstance INSTANCE = new C03_BeanWithDeprecatedGetInstance(); // ... } var bean = bc(C03_BeanWithDeprecatedGetInstance.class).run(); // ... }
Rationale:
When to Apply:
Purpose: Use proper utility classes instead of arrays for capturing mutable state in lambdas and test scenarios.
Rules:
Flag for boolean state: Instead of new boolean[]{false}, use Flag.create() for capturing boolean flags in lambdas and test hooks.IntegerValue for integer counters: Instead of new int[]{0}, use IntegerValue.create() for capturing integer counts in lambdas and test hooks.Examples:
// WRONG - Using arrays for state capture var hookCalled = new boolean[]{false}; bc(SimpleBean.class) .postCreateHook(b -> hookCalled[0] = true) .run(); assertTrue(hookCalled[0]); var callCount = new int[]{0}; bc(SimpleBean.class) .postCreateHook(b -> callCount[0]++) .run(); assertEquals(1, callCount[0]); // CORRECT - Using Flag and IntegerValue var hookCalled = Flag.create(); bc(SimpleBean.class) .postCreateHook(b -> hookCalled.set()) .run(); assertTrue(hookCalled.isSet()); var callCount = IntegerValue.create(); bc(SimpleBean.class) .postCreateHook(b -> callCount.increment()) .run(); assertEquals(1, callCount.get());
Flag Methods:
Flag.create() - Creates a flag initialized to falseflag.set() - Sets the flag to trueflag.isSet() - Returns true if flag is setflag.isUnset() - Returns true if flag is not setflag.setIf(condition) - Sets flag if condition is trueIntegerValue Methods:
IntegerValue.create() - Creates an integer initialized to 0counter.increment() - Increments by 1counter.get() - Returns the current valuecounter.getAndIncrement() - Returns current value then incrementscounter.incrementAndGet() - Increments then returns new valueRationale:
@Test void B_serialization() { var original = createTestBean(); var roundTrip = jsonRoundTrip(original, BeanClass.class); assertBean(original, roundTrip); }
@Test void E_strictMode() { // Test invalid value with strict mode assertThrowsWithMessage(RuntimeException.class, "Invalid value", () -> bean.setProperty("invalid")); // Test valid value with strict mode assertDoesNotThrow(() -> bean.setProperty("valid")); }
@Test void a08_otherGettersAndSetters() { var a = bean.addItems("item1", "item2"); assertBean(a, "items{0=item1,1=item2}"); var b = bean.setItems(Arrays.asList("item3", "item4")); assertBean(b, "items{0=item3,1=item4}"); }
When adding a new setting (configuration property) to a serializer or parser, follow these steps:
public static class Builder extends XmlSerializer.Builder { String textNodeDelimiter; // Add the field }
protected Builder() { textNodeDelimiter = env("XmlSerializer.textNodeDelimiter", ""); // Set default }
protected Builder(XmlSerializer copyFrom) { super(copyFrom); textNodeDelimiter = copyFrom.textNodeDelimiter; // Copy from serializer } protected Builder(Builder copyFrom) { super(copyFrom); textNodeDelimiter = copyFrom.textNodeDelimiter; // Copy from builder }
public Builder textNodeDelimiter(String value) { textNodeDelimiter = value == null ? "" : value; return this; }
This is essential to prevent caching issues where different configurations would incorrectly share the same cached instance:
@Override /* Context.Builder */ public HashKey hashKey() { return HashKey.of( super.hashKey(), addBeanTypesXml, addNamespaceUrisToRoot, // ... other fields ... textNodeDelimiter // ADD NEW SETTING HERE ); }
Why this is critical: Serializers and parsers use caching based on the hash key. If a new setting is not included in the hash key, two builders with different values for that setting will hash to the same key and incorrectly use the same cached instance, causing the second configuration to be ignored.
public class XmlSerializer extends WriterSerializer { final String textNodeDelimiter; // Add to main class public XmlSerializer(Builder builder) { super(builder); textNodeDelimiter = builder.textNodeDelimiter; // Initialize from builder } }
If the setting needs to be accessed during serialization:
public class XmlSerializerSession extends WriterSerializerSession { private final String textNodeDelimiter; protected XmlSerializerSession(Builder builder) { super(builder); textNodeDelimiter = ctx.textNodeDelimiter; // Get from context } }
If the serializer has subclasses (e.g., HtmlSerializer extends XmlSerializer), override the setter to maintain fluent API:
// In HtmlSerializer.Builder @Override public Builder textNodeDelimiter(String value) { super.textNodeDelimiter(value); // Call parent return this; // Return correct type }
hashKey() - This causes caching bugs where different configurations share the same cached instanceThis document serves as the definitive guide for unit testing in the Apache Juneau project, ensuring consistency, maintainability, and comprehensive test coverage.
When asked to “add to the release notes”, this refers to the current release file located at:
/docs/pages/release-notes/<VERSION>.md9.2.0/docs/pages/release-notes/9.2.0.mdRelease notes are organized into two main sections:
juneau-marshalljuneau-rest-commonjuneau-rest-serverjuneau-rest-clientjuneau-dtojuneau-microservicejuneau-examplesEach section should include: