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 AGENTS.md. General rules, permanent conventions, and best practices belong in AGENTS.md. Session-specific work progress, current tasks, and temporary patterns belong in AI_SESSION.md.
- "c" means "continue" - When the user sends just "c", continue with the current task or work
- "s" means "status" - When the user sends just "s", give a status update on what you're currently working on
- "TODO-x" means "work on this TODO" - When the user sends just "TODO-3", "TODO-67", etc., start working on that specific TODO item from the
/todo/TODO.mdfile
- "make a plan" or "come up with a plan" - When the user asks to make a plan for something, provide a summary of suggested changes only. Do NOT make actual code changes. The plan should outline what needs to be done, but implementation should wait for explicit user approval.
- "suppress warnings" or "suppress issues" - When the user asks to suppress warnings or issues that appear to refer to SonarLint/SonarQube issues, add
@SuppressWarningsannotations 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 Format: All
@SuppressWarningsannotations must use multi-line format with curly braces and include a brief comment explaining why each suppression is needed. Never use single-line@SuppressWarnings("xxx")without the multi-line format and comment.Or for multiple suppressions:@SuppressWarnings({ "resource" // CsvReader owns ParserReader; caller must close CsvReader })
Exception: Parameter-level annotations (e.g.@SuppressWarnings({ "rawtypes", // Raw types necessary for generic type handling "unchecked", // Type erasure requires unchecked casts "resource" // w is closed by try-with-resources; lambdas capture it })
catch (@SuppressWarnings("unused") Exception e)) may remain single-line when multi-line format is impractical.
- "start docs" or "start docusaurus" - Runs
scripts/start-docusaurus.py - "revert staged" - Runs
scripts/revert-staged.py - "revert unstaged" - Runs
scripts/revert-unstaged.py - "start jetty" - Runs
scripts/start-examples-rest-jetty.py - "start springboot" - Runs
scripts/start-examples-springboot.py - "push" or "push changes" - When the user asks to push changes, use
./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()" - "test" - Runs
scripts/test.py
- "save a rule" or "save this rule" - Add the rule/information to
AGENTS.md(permanent/general) - "store this rule in the session" - Add the rule/information to
AI_SESSION.md(session-specific) - "store this rule in the context" - Add the rule/information to
AGENTS.md(permanent/general)
- After each code modification, provide a brief (1-2 sentence) evaluation of the change
- Explain what was changed and why it improves the codebase
- This helps document the reasoning behind modifications and ensures changes are intentional
- Follow existing code patterns and conventions
- Maintain consistency with the existing codebase
- Use established naming conventions and formatting
- Preserve existing functionality while making improvements
When adding or modifying Java code, ALWAYS preserve the exact indentation of the surrounding code:
Critical Requirements:
- Use tabs, not spaces - Java files in this project use tab characters for indentation
- Match surrounding code exactly - Copy the leading whitespace character-for-character from the
old_string - Preserve indentation levels - Count tabs in nearby lines and use the same number
Common 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:
- Incorrect indentation (offset by one tab to the left) is a common error
- Mixed tabs/spaces cause inconsistent formatting
- IDE auto-formatting relies on consistent indentation
- Project standards mandate tab-based indentation
Verification Steps:
- Before creating
new_string, examine the indentation inold_string - Count the tab characters at the start of each line
- Copy the exact indentation pattern (including all tabs)
- For new Javadoc, match the indentation of nearby methods
Example 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
ThrowableUtilsmethods instead of direct exception constructors:- Use
illegalArg(String message, Object... args)forIllegalArgumentException - Use
runtimeException(String message, Object... args)for genericRuntimeException - Add static import:
import static org.apache.juneau.common.utils.ThrowableUtils.*;
- Use
-
Use MessageFormat-style placeholders with
ThrowableUtilsmethods:- Use
{0},{1}, etc. for parameter placeholders - Escape single quotes with
''(e.g.,"Value ''{0}'' is invalid") - Pass arguments as varargs after the message string
- Use
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
varkeyword whenever the type is obvious from the right-hand side:- Initializers with constructor calls:
var map = new HashMap<String,Integer>(); - Method calls with clear return types:
var list = getList(); - Stream operations:
var result = stream.collect(toList()); - Enhanced for loops:
for (var entry : map.entrySet())
- Initializers with constructor calls:
-
Keep explicit types when:
- The type is not obvious from the initializer:
InputStream stream = getStream(); - Readability would suffer:
boolean hasNext = iterator.hasNext();(better thanvar hasNext) - Generic type parameters need to be preserved on left side:
List<String> list = new ArrayList<>();
- The type is not obvious from the initializer:
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 iso, pattern variable iso2if (value instanceof Calendar value2)- original variable isvalue, pattern variable isvalue2if (this instanceof MethodInfo this2)- original variable isthis, pattern variable isthis2
-
Exception 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 iso2, pattern variable iso3if (o1 instanceof Comparable o3)- original variable iso1, pattern variable iso3(noto12)
-
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 byvalue2.forEachValue(..., (pMeta2, key2, value3, thrown2) -> ...)- lambda parameter usesvalue3to avoid conflict with pattern variablevalue2
Examples:
// 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 value2When declaring class fields, always use final to ensure true immutability:
-
Always use
finalfor fields:- All instance fields should be declared
finalwhenever possible - This provides compile-time immutability guarantees
- Prevents accidental modification after construction
- All instance fields should be declared
-
Use
findhelper methods for memoized fields:- When memoized
Supplierfields need constructor parameters, use helper methods - Name helper methods with the
findprefix (e.g.,findGenericInterfaces()) - Helper methods are called during field initialization and can access constructor parameters
- This allows final fields to depend on constructor parameters
- When memoized
-
Why this matters:
- Provides stronger immutability guarantees than "effectively final" comments
- Compiler enforces immutability rather than relying on conventions
- Makes the code more maintainable and less error-prone
- Documents thread-safety guarantees explicitly
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:
- Declare constructor parameters as
finalfields - Create
findhelper methods that use those fields - Initialize memoized
Supplierfields with method references to the helper methods - This allows all fields to be truly
finalwhile still supporting lazy initialization
When working with git operations, especially reverting changes:
-
Use helper scripts for reverting - Always use the provided Python scripts instead of git commands directly:
- ✅ CORRECT:
./scripts/revert-unstaged.py path/to/specific/File.java(reverts unstaged to staged) - ✅ CORRECT:
./scripts/revert-staged.py path/to/specific/File.java(reverts to HEAD, discards all changes) - ❌ WRONG: Using
git restore,git checkout, or any git commands directly
- ✅ CORRECT:
-
Revert Unstaged Changes Script -
./scripts/revert-unstaged.py- Reverts working directory changes back to the staged (INDEX) version
- Preserves staged changes that have been tested
- Use this when you have staged changes you want to keep
- Command:
git restore --source=INDEX <file>
-
Revert Staged Changes Script -
./scripts/revert-staged.py- Reverts both staged AND unstaged changes back to HEAD (last commit)
⚠️ WARNING: Discards all changes (staged and unstaged)- Use this when you want to completely discard all changes to a file
- Command:
git restore --source=HEAD <file>
-
Always revert one file at a time - Never use broad wildcards or multiple file paths:
- ✅ CORRECT:
./scripts/revert-unstaged.py path/to/specific/File.java - ❌ WRONG: Reverting multiple files or using wildcards
- ✅ CORRECT:
-
Why this matters:
- Preserves staged changes: Staged changes have been tested and should not be lost
- Surgical precision: Only reverts the specific problematic file's changes
- Safe recovery: If a file is staged, it means it was working at that point
- Prevents data loss: Won't accidentally revert good changes along with bad ones
- User-friendly: Scripts provide clear feedback and prevent common mistakes
-
Proper workflow when compilation fails:
- Identify the specific file(s) causing the error
- Revert only that file's unstaged changes:
./scripts/revert-unstaged.py path/to/ProblematicFile.java - Verify the revert fixed the issue (back to staged/tested version)
- Then address that specific file's changes separately
Examples:
# 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/fileUnderstanding the scripts:
revert-unstaged.py: Usesgit restore --source=INDEXto revert to staged versionrevert-staged.py: Usesgit restore --source=HEADto revert to last commit- Both scripts handle one file at a time for safety
- Both provide clear feedback about what they're doing
- Ensure comprehensive test coverage for all changes
- Follow the established unit testing patterns
- Use the Sequential Single-Letter Label Convention (SSLLC)
- Implement proper assertion patterns using
assertBean()
- Maintain comprehensive javadoc documentation
- Follow established documentation formatting rules
- Include practical examples in documentation
- Link to relevant specifications and resources
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:
- Code changes not being reflected in test runs
- Old code behavior persisting despite source changes
- Stale class files causing incorrect results
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:
- Ask the user to check if "Build Automatically" is enabled (Project → Build Automatically menu)
- If enabled, politely ask them to disable it: "Could you please disable 'Build Automatically' in Eclipse (Project → Build Automatically)?"
- Why this matters:
- When "Build Automatically" is enabled, Eclipse may rebuild files immediately after saving
- This can conflict with Maven builds, causing files to be overwritten
- Disabling it gives you more control over when builds occur
- The user can manually build when needed using Ctrl+B or Project → Build Project
Resolution Strategy: If you encounter situations where your code changes don't appear to be taking effect:
- First, ask about Eclipse "Build Automatically" and request it be disabled if enabled
- Then run
mvn clean installto clear all compiled artifacts and rebuild fresh - This ensures Maven and IDE compiled code are synchronized
- Rerun tests after the clean build to verify changes are properly reflected
When to suspect this issue:
- Tests fail in unexpected ways after code changes
- Behavior doesn't match recent code modifications
- Test results seem to reflect old code despite edits
- Compilation succeeds but runtime behavior is wrong
- Code changes appear to be getting overwritten or not taking effect
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:
- Java 17 can be found at:
~/jdk/openjdk_17.0.14.0.101_17.57.18_aarch64/bin/java - Use this path when you need to run Java commands directly
Build and Test Script:
A reusable Python script is available at scripts/test.py for common Maven operations.
Usage:
# Default: Clean build + run tests
./scripts/test.py
# Build only (skip tests)
./scripts/test.py --build-only
./scripts/test.py -b
# Test only (no build)
./scripts/test.py --test-only
./scripts/test.py -t
# Full build and test (explicit)
./scripts/test.py --full
./scripts/test.py -f
# Verbose output (show full Maven output)
./scripts/test.py --verbose
./scripts/test.py -v
# Help
./scripts/test.py --help
./scripts/test.py -hWhat it does:
--build-only/-b: Runsmvn clean install -q -DskipTests--test-only/-t: Runsmvn test -q -Drat.skip=true--full/-f(default): Runs both build and test in sequence- By default, shows only the last 50 lines of output
- Use
--verbose/-vto see full Maven output
When to use:
- Instead of manually typing out Maven commands repeatedly
- When you need to verify both build and tests pass
- During iterative development to quickly test changes
A reusable Python script is available at scripts/coverage.py for checking JaCoCo test coverage on any source file, package, or module.
When asked for current test coverage on a class, package, or module, always use this script rather than manually parsing JaCoCo output.
Usage:
# Coverage for an entire package
./scripts/coverage.py juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/conversion/
# Coverage for a single file
./scripts/coverage.py juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/conversion/BasicConverter.java
# Show only lines with missed branches (hide instruction-only misses)
./scripts/coverage.py path/to/file.java --branches
# Re-run tests first to refresh coverage data, then report
./scripts/coverage.py path/to/folder/ --runWhat it does:
- Detects the Maven module automatically from the path
- Generates a JaCoCo report using the existing
juneau-utest/target/jacoco.exec - Displays branch and instruction coverage percentages with a progress bar
- Lists every uncovered line with the number of missed branches/instructions
How coverage data works:
- The
.execfile is produced byjuneau-utestduring a test run and covers all loaded classes, including those from other modules (e.g.juneau-commons) - Use
--runwhen tests have changed since the last run; omit it to reuse existing data for speed - Report generation takes ~2 seconds; test execution adds ~10-15 seconds
Trigger phrases: When the user says any of the following, run this script:
- "what's the coverage on ..."
- "show coverage for ..."
- "current test coverage"
- "check coverage"
- "coverage report"
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
timeoutfor commands withhead/tail:- Instead of:
command | grep pattern | head -5 - Use:
timeout 30s sh -c 'command | grep pattern | head -5'
- Instead of:
-
Timeout durations:
- Quick commands (grep, find, etc.): 10-30 seconds
- Maven commands: 60-120 seconds (or longer for full builds)
- Adjust based on expected command duration
-
Why this matters:
- Commands with
head/tailcan hang indefinitely if the input stream doesn't close - Pipe operations may wait for input that never arrives
- Timeouts prevent the assistant from getting stuck waiting for commands that will never complete
- Commands with
Examples:
# 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'- Use
assertThrowsWithMessagefor exception testing - Test both valid and invalid scenarios
- Include proper error messages in assertions
- Test null parameter validation where applicable
- Aim for 100% instruction coverage on bean classes
- Use JaCoCo reports to identify missing coverage
- Focus on methods with 0% coverage first
- Add comprehensive tests for all code paths
- Hard-to-Test Code Marking: When a line of code is found to not be fully testable (e.g., requires complex setup, compiler-generated code, or unreachable branches), add a comment
// HTT(Hard To Test) on that line to document why it's difficult to test - When writing unit tests for new code, always run
coverage.pyafterward to identify coverage gaps before moving on. Use--runto refresh the.execdata after adding new tests:./scripts/coverage.py path/to/NewFile.java --run ./scripts/coverage.py path/to/new/package/ --run ./scripts/coverage.py path/to/new/package/ --branches # show only missed branches
- Follow established file naming conventions
- Maintain proper package structure
- Use consistent import organization
- Follow established class and method naming patterns
- Use hardcoded links to
https://juneau.apache.org/docs/topics/ - Include specification links for external standards where applicable
- Use proper cross-references with
{@link}tags - Maintain consistent link formatting
- Work through tasks systematically and alphabetically when specified
- Complete tasks without breaks when requested
- Verify all changes work correctly
- Maintain consistency across similar files
- When the user says "add to TODO" or "add to the TODO list", this refers to the
TODO.mdfile in the/todofolder - Do NOT use the in-memory todo list tool for user-requested TODO items
- Add items directly to the
/todo/TODO.mdfile using the write or search_replace tools - Follow the existing format and structure of the TODO.md file
- TODO Identifiers: When adding new TODO items, assign them a unique "TODO-#" identifier (e.g., "TODO-1", "TODO-2", etc.)
- TODO References: When the user asks to "fix TODO-X" or "work on TODO-X", they are referring to the specific identifier in the TODO.md file
- TODO Completion: When TODOs are completed, remove them from the TODO list entirely
- Plans: Implementation plans (e.g.,
1_ini_implementation.md,2_hjson_implementation.md) are also located in the/todofolder alongside TODO.md
- When the user says "add to release notes" or "add this to the release notes", this refers to the release notes in the
/juneau-docsdirectory - Do NOT add to the root
RELEASE-NOTES.txtfile - For the current release (9.2.0), add entries to
/juneau-docs/9.2.0.md - Follow the existing format and structure of the release notes in juneau-docs
- Use the same formatting style as other entries in the release notes file
When adding fluent setter overrides to classes:
- Include blank lines between each method
- Each override method should be separated by exactly one blank line
- Example pattern:
@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; }
- Rationale: Maintains consistency with existing code style and improves readability
When creating a properties() method that uses the fluent map API with .a() method calls:
- Always use
Utils.filteredBeanPropertyMap(): UsefilteredBeanPropertyMap()to create the map. This method provides sorted, filtered, fluent maps specifically designed for bean property maps. - Wrap in formatter comments: Wrap the entire method contents inside
// @formatter:offand// @formatter:oncomments to preserve formatting
protected 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 API- Ensures consistent ordering for deterministic output
- Provides a clean, dedicated API for building bean property maps
- Formatter comments preserve the formatting of fluent method chains and prevent formatters from reformatting the code in ways that reduce readability
This document outlines the documentation conventions, formatting rules, and best practices for the Apache Juneau project.
Java Variables in Examples:
- Wrap local Java variables in
<jv>tags - Example:
<jv>json</jv>,<jv>swagger</jv>,<jv>result</jv>
Static Method References:
- Use
<jsm>tags for static method names - Example:
Json.<jsm>from</jsm>(<jv>x</jv>)
Static Field References:
- Use
<jsf>tags for static field names - Example:
JsonSerializer.<jsf>DEFAULT</jsf>
Code Comments:
- Use
<jc>tags for code comments - Example:
<jc>// Serialize using JsonSerializer.</jc>
Class References:
- Use
<jk>tags for class names in text - Example:
<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 block
Standard 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:
- Use
valueas the parameter name for fluent setters - Document parameter types and constraints
- Specify when parameters can be null
Builder Method Parameter Naming:
- For single-value setter methods in builder classes, use
valueas the parameter name - This allows field assignment without the
this.modifier (e.g.,field = valueinstead ofthis.field = value) - Improves code readability and reduces redundancy
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:
- Use hardcoded links to
https://juneau.apache.org/docs/topics/ - Use slug names as topic names
- Use page titles for anchor text
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:
- Use
{@link ClassName}for internal class references - Use
{@link ClassName#methodName}for method references
External References:
- Use
<a class="doclink" href="URL">text</a>for external links - Include specification links for external standards where applicable
Standard 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:
- Always include both
Json.from()andtoString()examples - Use consistent variable names (
<jv>json</jv>,<jv>x</jv>) - Include explanatory comments
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) {- Use consistent parameter names (
valuefor fluent setters) - Use consistent variable names in examples
- Use consistent link formatting
- Document all public methods and constructors
- Include parameter types and constraints
- Include return value descriptions
- Include exception documentation
- Use clear, concise descriptions
- Include practical examples
- Link to relevant specifications
- Explain complex behavior
- Keep documentation up-to-date with code changes
- Use consistent formatting throughout
- Include cross-references where helpful
- Document edge cases and special behavior
/**
* 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:
- Single values: Use single letters (
a,b,c, etc.) - Multiple values on same line: Append numbers (
a1,a2,b1,b2, etc.) - New bean instances: Reset labels to 'a' when creating new bean instances
- Consistent prefixes: Use consistent prefixes for multiple values on the same line
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 testing
Examples:
// 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:
- L is a letter from 'a'-'z' representing the test category
- NN is a number from "00"-"99" for ordering tests within a category
- testName is a descriptive name for what the test does
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:
- Tests are grouped by letter prefix (a, b, c, etc.)
- Each group represents a general test category
- Tests within a group are numbered sequentially
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:
- Use
@Nestedinner classes for major test categories - Each nested class name follows the pattern:
L_categoryNamewhere L is an uppercase letter (A, B, C, etc.) - Tests within nested classes use
l##_testNamepattern where l is the lowercase letter matching the nested class (a, b, c, etc.) and ## is an incrementing number (01, 02, 03, etc.) - The letter in the nested class name matches the letter in the test method names (uppercase for class, lowercase for methods)
- Each nested class extends
TestBaseto inherit test utilities and setup
While not prescriptive, common test category prefixes include:
- a: Basic tests (properties, constructors, basic functionality)
- b: Serialization/deserialization tests
- c: Extra/dynamic properties tests
- d: Additional methods tests
- e: Validation/strict mode tests
- f: Reference resolution tests (where applicable)
Choose the structure (Simple vs Complex) based on:
- Simple: < 20 tests, or tests don't naturally group into major categories
- Complex: > 20 tests, especially when tests group into distinct functional areas
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 adder
Examples:
// 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:
- A_basicTests: Use the varargs version (e.g.,
setTags("tag1", "tag2")) - D_additionalMethods: Test the Collection version (e.g.,
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.
- Bean Classes: Aim for 100% instruction coverage
- UI Classes: Can be excluded from coverage targets
- Builder Classes: Include comprehensive tests for all builder methods
- Use
coverage.pyto identify missing coverage — always run it after writing unit tests for new code:./scripts/coverage.py path/to/NewFile.java --run ./scripts/coverage.py path/to/new/package/ --run ./scripts/coverage.py path/to/new/package/ --branches
- Focus on methods with 0% coverage first
- Add tests for uncovered code paths
- Verify coverage improvements after adding tests
- Use SSLLC for consistent test data naming
- Reset labels when creating new bean instances
- Use meaningful test data that represents real-world scenarios
- Prefer
assertBean()over individual property assertions - Use deep property paths for comprehensive validation
- Test both positive and negative scenarios
- Group related tests in logical test methods
- Use descriptive test method names
- Follow consistent test structure across all test classes
Rule: 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:
- L is the uppercase letter matching the test method prefix (A, B, C, etc.)
- NN is the two-digit number matching the test method number (01, 02, 03, etc.)
- ClassName is the descriptive class name
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:
- Makes it immediately clear which test uses the helper class
- Reduces namespace pollution by scoping helper classes to their tests
- Improves code locality by keeping related code together
- Makes it easier to identify unused helper classes
When to Apply:
- Only apply this convention to helper classes used by a single test
- Classes used by multiple tests should keep their generic names
- Classes used across nested test classes should remain at the nested class level
Purpose: Use proper utility classes instead of arrays for capturing mutable state in lambdas and test scenarios.
Rules:
- Use
Flagfor boolean state: Instead ofnew boolean[]{false}, useFlag.create()for capturing boolean flags in lambdas and test hooks. - Use
IntegerValuefor integer counters: Instead ofnew int[]{0}, useIntegerValue.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 tofalseflag.set()- Sets the flag totrueflag.isSet()- Returnstrueif flag is setflag.isUnset()- Returnstrueif flag is not setflag.setIf(condition)- Sets flag if condition istrue
IntegerValue Methods:
IntegerValue.create()- Creates an integer initialized to0counter.increment()- Increments by 1counter.get()- Returns the current valuecounter.getAndIncrement()- Returns current value then incrementscounter.incrementAndGet()- Increments then returns new value
Rationale:
- Provides cleaner, more readable code than array access patterns
- Better type safety and API design
- Consistent with Apache Juneau utility class patterns
- Makes test code more maintainable and easier to understand
- Include javadoc for test methods explaining their purpose
- Document any special test scenarios or edge cases
- Keep test code readable and maintainable
@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
}- ❌ Forgetting to add the setting to
hashKey()- This causes caching bugs where different configurations share the same cached instance - ❌ Not copying the field in all copy constructors
- ❌ Not overriding setter methods in subclass builders
- ❌ Not passing the setting to the session if it's needed during serialization
This 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>.md- Current version:
9.2.1 - Current file:
/docs/pages/release-notes/9.2.1.md
Release notes are organized into two main sections:
- Top-level major changes - High-level overview at the beginning of the file listing significant changes
- Per-module updates - Detailed changes organized by module (similar to 9.0.0.md structure):
juneau-marshalljuneau-rest-commonjuneau-rest-serverjuneau-rest-clientjuneau-dtojuneau-microservicejuneau-examples- Other modules as applicable
Each section should include:
- New features
- Bug fixes
- Breaking changes
- Deprecations
- Performance improvements
- Documentation updates
- API changes
- Read the current release notes file (9.2.0.md) to understand the existing structure
- Determine if the change is a major change (top-level) or module-specific
- Add new entries under the appropriate section and module
- Use clear, concise descriptions with code examples where helpful
- Include issue/PR references where applicable
- Maintain consistent formatting with existing entries (see 9.0.0.md for reference)