| brXM | B.R.U.T |
|---|---|
| 16.7.0 | 5.3.0 |
| 16.7.0 | 5.2.0 |
| 16.6.5 | 5.1.0 |
| 16.6.5 | 5.0.1 |
| 16.0.0 | 5.0.0 |
| 15.0.1 | 4.0.1 |
| 15.0.0 | 4.0.0 |
| 14.0.0-2 | 3.0.0 |
| 13.4 | 2.1.2 |
| 13.1 | 2.0.0 |
| 12.x | 1.x |
Page Model API v1.0 Document Resolution + Ergonomic Content API + Test Performance
This release fixes silent data loss when parsing Page Model API v1.0 responses, adds helper methods that replace 4-step stream pipelines with single calls, and significantly reduces test-suite startup overhead through JCR repository sharing.
- v1.0 document resolution —
PageModelResponsenow correctly handles v1.0 responses where documents and imagesets live inside thepagesection (no separatecontentsection). Previously, Jackson silently dropped thedatafield on these entries becausepagewas typedMap<String, PageComponent>. A new@JsonSetter("page")splits entries bytype:documentandimagesetentries route to an internaldocumentsmap; component entries stay inpage. resolveContentv1.0 fallback —resolveContent(ref)previously only checkedcontent(v1.1). It now also checks thedocumentsmap, so/page/-scoped$refvalues resolve in v1.0 responses.hasContent()recognizes v1.0 — Returnstruewhendocumentsis non-empty, even whencontentis absent.getComponentCount()excludes documents/imagesets — Counts only component entries. Documents and imagesets parsed from the v1.0pagemap are no longer included in the count.
resolveModelContent(component, modelName, type)— Resolves a single$reffrom a component model to a typedOptional<T>. Works with both/content/(v1.1) and/page/(v1.0) refs.resolveModelContentList(component, modelName, type)— Resolves a list of$refobjects from a component model toList<T>. Same dual-format support.getDocuments()— Exposes the parsed document/imageset map for v1.0 responses.
Before (manual 4-step pipeline):
List<ArticleData> articles = component.<List<Map<String, Object>>>getModel("items")
.stream()
.map(m -> PageModelMapper.INSTANCE.convertValue(m, ContentRef.class))
.map(pageModel::resolveContent)
.filter(Objects::nonNull)
.map(item -> item.as(ArticleData.class))
.toList();After (one call):
List<ArticleData> articles = pageModel.resolveModelContentList(component, "items", ArticleData.class);| Caller | Impact |
|---|---|
setPage(Map<String, PageComponent>) |
Preserved — programmatic callers unaffected; documents stays empty |
getPage() |
Returns only components — behavioural change for v1.0 responses: documents/imagesets no longer appear here (this is the correct behaviour) |
resolveContent() |
New fallback only — existing /content/ behaviour unchanged |
getComponentCount() |
Counts only components — behavioural change for v1.0 responses (fix, not regression) |
New org.bloomreach.forge.brut.resources.diagnostics package. Bare AssertionError failures now include structured, actionable output pointing to the exact YAML file or configuration node that needs fixing.
PageModelAssert — Fluent assertions that embed diagnostic output on failure:
PageModelAssert.assertThat(pageModel, "/news", "mysite")
.hasPage("newsoverview")
.hasComponent("NewsList")
.containerNotEmpty("main");Failure output example:
[ERROR] Expected page 'newsoverview' but got 'pagenotfound'
RECOMMENDATIONS:
• Add sitemap entry in: hst/configurations/mysite/sitemap.yaml for path: /news
• Ensure sitemap entry points to: hst:pages/newsoverview
• Create page configuration in: hst/configurations/mysite/pages/newsoverview.yaml
• Example sitemap entry:
/news:
jcr:primaryType: hst:sitemapitem
hst:componentconfigurationid: hst:pages/newsoverview
| Assertion | What it diagnoses |
|---|---|
hasPage(name) |
pagenotfound, wrong page — sitemap and page config guidance |
hasComponent(name) |
Missing component — lists all available component names |
containerNotEmpty(name) |
Empty container — workspace reference and child component guidance |
containerHasMinChildren(name, n) |
Insufficient children count |
hasContent() |
No content items in response |
componentHasModel(comp, model) |
Missing model on a named component |
PageModelDiagnostics — Manual diagnostic calls returning DiagnosticResult without failing the test:
diagnosePageNotFound(expectedPage, path, pageModel)diagnoseComponentNotFound(name, requestUri, pageModel)diagnoseEmptyContainer(containerName, pageModel)diagnoseEmptyResponse(requestUri)
ConfigurationDiagnostics — Diagnoses ConfigService bootstrap failures. Walks the full exception chain and identifies:
- YAML parse errors — with file name, line number, and column
- Circular module dependencies — suggests checking
after:inhcm-module.yaml - Duplicate node names — identifies conflicting definitions
- Missing property definitions — names the property, namespace, and node type
- Invalid node types — names the type and namespace prefix
ConfigErrorParser — Extracts structured data from ConfigurationRuntimeException messages: JCR paths, YAML file references, node types, property issues.
DiagnosticResult — Java record: severity(), message(), recommendations(). toString() formats with [ERROR]/[WARN]/[OK] prefix and bullet-pointed recommendations.
DiagnosticSeverity — SUCCESS, INFO, WARNING, ERROR.
-
JCR repository sharing —
BrxmComponentTestExtensionnow reuses a singleBrxmTestingRepositoryacross all test classes that share the same configuration fingerprint (bean packages, test resource path, content YAML, and content root). Jackrabbit is bootstrapped once per fingerprint and shut down once at the end of the suite via a JUnit 5CloseableResourcein the root store. Each test class still opens its own JCR session and gets a fresh set of mock objects. -
ConfigurationModel caching —
ConfigServiceBootstrapStrategycaches the builtConfigurationModelkeyed by the SHA-256 fingerprint of its source files. Subsequent test classes with the same HCM modules skip the fullConfigServiceparse step, avoiding repeated YAML/CND processing. -
Node type registration guard —
BaseComponentTest.shouldRegisterBaseNodeTypes()delegates toBrxmTestingRepository.recordInitialization("__baseNodeTypes__"). On a shared repository the ~50hasNodeType()calls that previously ran at the start of every test class now run at most once per repository. -
BrxmTestingRepository.recordInitialization(key)— New first-caller-wins gate. Returnstruethe first time a key is seen (proceed with the operation) andfalseon subsequent calls (skip). Used internally byBaseComponentTestfor both node-type registration and skeleton YAML import. -
BaseComponentTest.setRepository(BrxmTestingRepository)— New API that allows a pre-bootstrapped repository to be injected beforesetup()is called. The extension uses this to share a single repository instance across test classes. -
RuntimeTypeStubber log level — Stub-specific messages in
RuntimeTypeStubberraised back from INFO/DEBUG to WARN (reverting the normalization applied in 5.2.0 for these messages only). These stubs indicate missing node type definitions in the project's CND files and warrant developer attention.
Class-level parallel execution (multiple test classes running concurrently) is supported with the following caveats:
- Each class receives its own JCR session,
DynamicComponentTestinstance, and mock objects. getOrComputeIfAbsent()in the JUnit 5 root store guarantees a single bootstrap per fingerprint.BrxmTestingRepository.appliedInitKeysuses aCollections.synchronizedSet— individualadd()calls are atomic, but callers should not assume the associated operation is complete immediately after another thread'sadd()returnsfalse.
Method-level parallel execution (@Execution(CONCURRENT) on test methods within the same class) is not supported. All methods in a class share a single DynamicComponentTest instance, a single JCR session, and non-thread-safe mock objects. Enabling method-level concurrency will cause data corruption and intermittent failures.
brXM 16.7.0 Compatibility & Security Hardening
This release upgrades the brXM parent POM to 16.7.0 and includes breaking changes in transitive dependencies.
- brXM 16.7.0 required — Parent POM upgraded from
hippo-cms7-project:16.6.5to16.7.0. Projects on 16.6.x must remain on BRUT 5.1.x. - commons-lang3 migration — Internal Spring config references updated from
org.apache.commons.lang.StringUtilstoorg.apache.commons.lang3.StringUtils, matching brXM 16.7.0's classpath. Projects overridinghst-manager.xmlmust update their copies.
- CVE-2025-48924 — CI workflow hardened to prevent credential exposure: heredoc quoting (
<< 'EOF'), Maven${env.*}syntax for server credentials, and fork PR builds are now skipped (secrets are unavailable to fork workflows).
- Fork PR handling — CI detects fork PRs and skips the build with a notice, instead of failing with 401 Unauthorized on the private Maven repository.
- Log level normalization — Operational messages from
ConfigServiceBootstrapStrategy,JcrNodeSynchronizer,ConfigServiceReflectionBridge, andSiteContentBaseResolverValvedowngraded from WARN to INFO/DEBUG. These are expected conditions during test bootstrap, not warnings. Note:RuntimeTypeStubbermessages were temporarily downgraded here but raised back to WARN in 5.3.0, as stubs indicate potentially missing CND definitions. - Log noise reduction —
VirtualHostsService(duplicate mount alias errors) suppressed;HstDelegateeFilterBeanreduced from ALL to WARN. - Javadoc scope warnings —
@BrxmComponentTest,@BrxmJaxrsTest, and@BrxmPageModelTestannotations now document the<scope>test</scope>requirement and the consequences of omitting it.
Annotation-Based Testing & Production Parity
This release introduces a completely new annotation-based testing API with convention-over-configuration design, plus production-parity HST configuration via brXM's ConfigurationConfigService.
Quick Start Guides:
- Getting Started - First test in 3 steps
- Quick Reference - Annotation options at a glance
- Common Patterns - Recipes for all test types
- Troubleshooting - Common issues and fixes
Three new annotations provide zero-boilerplate testing with automatic field injection - no inheritance required:
| Annotation | Use Case | Injected Field |
|---|---|---|
@BrxmComponentTest |
HST component testing | DynamicComponentTest |
@BrxmPageModelTest |
Page Model API testing | DynamicPageModelTest |
@BrxmJaxrsTest |
JAX-RS endpoint testing | DynamicJaxrsTest |
Minimal Example (Parameter Injection - Recommended):
@BrxmComponentTest // Zero config for standard project layouts!
public class MyComponentTest {
@Test
void testComponent(DynamicComponentTest brxm) { // Parameter injection - no IDE warnings!
assertNotNull(brxm.getHstRequest());
assertTrue(brxm.getRootNode().hasNode("hippo:configuration"));
}
}Alternative: Field Injection (use when you need the instance in @BeforeEach or nested classes):
@BrxmComponentTest
public class MyComponentTest {
@SuppressWarnings("unused") // Injected by extension
private DynamicComponentTest brxm;
@BeforeEach
void setup() {
brxm.setSiteContentBasePath("/content/documents/mysite");
}
}Key Features:
- Auto-detection of bean packages - Discovers packages from
project-settings.xml, classpath scanning, orpom.xml - Auto-detection of node types - Scans
@Node(jcrType="...")annotations automatically - Auto-detection of HST root - Derives from project name or Maven artifactId
- Auto-detection of test resources - Finds YAML/CND files in conventional locations
- Parameter injection (recommended) - Standard JUnit 5 pattern with no IDE warnings
- Field injection (alternative) - For
@BeforeEach/@Nestedscenarios - Nested test support - Full JUnit 5
@Nestedclass support with field inheritance
Bean Package Auto-Detection Strategy Chain:
| Priority | Strategy | Source |
|---|---|---|
| 10 | ProjectSettingsStrategy | project-settings.xml (selectedProjectPackage, selectedBeansPackage) |
| 20 | ClasspathNodeAnnotationStrategy | Scans classpath for @Node-annotated classes |
| 30 | PomGroupIdStrategy | Derives from pom.xml groupId |
| 40 | TestClassHeuristicStrategy | Appends .beans/.model/.domain to test package |
Explicit Configuration (when needed):
@BrxmJaxrsTest(
beanPackages = {"org.example.model"}, // Override auto-detection
resources = {HelloResource.class}, // JAX-RS resources
loadProjectContent = true // Use production HCM modules
)New Classes:
brut-components:BrxmComponentTest,DynamicComponentTest,BrxmComponentTestExtensionbrut-resources:BrxmPageModelTest,BrxmJaxrsTest,DynamicPageModelTest,DynamicJaxrsTestbrut-common(strategy package):BeanPackageStrategy,DiscoveryContext,BeanPackageStrategyChain,ProjectSettingsStrategy,ClasspathNodeAnnotationStrategy,PomGroupIdStrategy,TestClassHeuristicStrategy
Major refactoring following Uncle Bob's Clean Code principles:
Consolidated Exception Handling:
- Unified
BrutTestConfigurationExceptioninbrut-commonreplaces duplicate exception classes - Factory methods for semantic error creation:
missingAnnotation(),missingField(),setupFailed(),bootstrapFailed(),resourceNotFound()
Consolidated Logging:
- Unified
TestConfigurationLoggerinbrut-commonprovides consistent configuration logging - Methods:
logConfiguration(),logSuccess(),logFailure(),logBeanPatterns(),logSpringConfigs()
Extracted Responsibilities from ConfigServiceBootstrapStrategy:
RuntimeTypeStubber- Handles runtime stubbing of missing JCR namespaces and node typesJcrNodeSynchronizer- Handles JCR node synchronization between source and target treesConfigServiceReflectionBridge- Encapsulates reflection calls to ConfigService methods- Result: ConfigServiceBootstrapStrategy reduced from ~1650 to ~1180 lines (28% reduction)
Shared Utilities:
TestInstanceInjectorinbrut-common- Shared field injection for JUnit 5 extensionsAbstractBrutRepositoryinbrut-common- Shared CND registration and namespace handling
Key Features:
- ConfigServiceRepository - New repository implementation using brXM's production ConfigService for HST bootstrap
- Production-Identical Structure - HST configuration created using the exact same code path as production brXM
- Zero Maintenance - Changes in brXM's HST structure propagate automatically (no manual updates needed)
- Explicit Module Loading - Uses ModuleReader to load only test HCM modules (avoids framework dependency conflicts)
- Works with Both Test Types - Compatible with AbstractJaxrsTest and AbstractPageModelTest
- Strategy Pattern - Pluggable bootstrap strategies (ConfigService or manual fallback)
Usage:
Requires HCM module descriptor and configuration files:
// 1. Create HCM module descriptor: src/test/resources/META-INF/hcm-module.yaml
group:
name: myproject-test
project: myproject-test
module:
name: test-config
// 2. Create HCM config: src/test/resources/hcm-config/hst/demo-hst.yaml
definitions:
config:
/hst:myproject:
jcr:primaryType: hst:hst
/hst:myproject/hst:configurations:
jcr:primaryType: hst:configurations
# ... your HST structure
// 3. Override repository in Spring XML
<bean id="javax.jcr.Repository"
class="org.bloomreach.forge.brut.resources.ConfigServiceRepository"
init-method="init" destroy-method="close">
<constructor-arg ref="cndResourcesPatterns"/>
<constructor-arg ref="contributedCndResourcesPatterns"/>
<constructor-arg ref="yamlResourcesPatterns"/>
<constructor-arg ref="contributedYamlResourcesPatterns"/>
<constructor-arg value="myproject"/>
</bean>
// 4. Use in tests
@Override
protected List<String> contributeSpringConfigurationLocations() {
return Arrays.asList("/org/example/config-service-jcr.xml");
}Documentation:
- See
user-guide/config-service-repository.mdfor detailed usage guide - Example integration tests in
demo/site/components/src/test/java/org/example/
Architecture:
- Uses
ModuleReaderfor explicit module loading (no classpath scanning) - Reflection-based access to package-private ConfigService methods
- Bootstrap strategy pattern for flexible initialization
MockHstRequest now supports HTTP sessions via Spring's MockHttpSession:
// Get or create session (lazy initialization)
HttpSession session = brxm.getHstRequest().getSession();
session.setAttribute("user", userProfile);
// Inject a mock session
brxm.getHstRequest().setSession(mockSession);
// Manual session cleanup
brxm.getHstRequest().invalidateSession();Session Isolation:
- Sessions are automatically invalidated in
setupForNewRequest()for multi-test classes - Prevents session state from leaking between tests
- Works with JAX-RS and PageModel tests
Production-parity configuration simplified to a single parameter:
@BrxmJaxrsTest(
loadProjectContent = true // That's it - beanPackages auto-detected!
)Automatically uses ConfigServiceRepository with HCM modules - no manual Spring XML configuration required.
Implementation Details:
- Auto-generates Spring configuration with ConfigServiceRepository bean
- Loads minimal framework module for core brXM node types (editor, hipposysedit, webfiles)
- Works with all 3 annotation types (PageModel, JAX-RS, Component)
- Permissive CNDs ensure test bootstrapping success
Benefits:
- Zero Spring XML boilerplate for ConfigService setup
- Production-parity configuration in tests
- Explicit HCM module loading (no classpath pollution)
Minimal Framework CNDs: The embedded minimal framework uses permissive node type definitions to prioritize test execution over strict validation. This is acceptable for unit testing scenarios and documented in the CND headers.
Smart convention-over-configuration improvements for annotation-based tests:
Multi-File Spring Config Detection:
- Automatically detects multiple Spring XML files per test type
- JAX-RS tests check:
custom-jaxrs.xml,annotation-jaxrs.xml,rest-resources.xml,jaxrs-config.xml - PageModel tests check:
custom-pagemodel.xml,annotation-pagemodel.xml,custom-component.xml,component-config.xml - All matching files are automatically included
ConfigService-Aware Resource Detection:
- CND patterns auto-detected only when
loadProjectContent=false(avoids conflicts) - YAML patterns auto-detected only when
loadProjectContent=false(avoids conflicts) - Prevents duplicate loading between ConfigService and legacy import mechanisms
Example:
@BrxmJaxrsTest(beanPackages = {"org.example.model"})
// NO springConfigs needed - auto-detects custom-jaxrs.xml + rest-resources.xml
public class MyTest {
private DynamicJaxrsTest brxm;
@Test
void test() {
// Both Spring configs loaded automatically
}
}Benefits:
- Less configuration required in annotations
- Supports projects with multiple Spring config files
- Intelligent conflict prevention with ConfigService
- Explicit annotation values always override auto-detection
Chainable APIs for common test operations:
Typed JSON Responses with executeAs():
// Two lines become one - JSON deserialization built into the fluent API
User user = brxm.request()
.get("/site/api/user/123")
.executeAs(User.class);
assertEquals("John", user.getName());Request Body with withJsonBody():
// POST with JSON body - sets Content-Type automatically
User created = brxm.request()
.post("/site/api/users")
.withJsonBody("{\"name\": \"John\"}")
.executeAs(User.class);
// Or serialize an object directly
User input = new User("Jane", 25);
brxm.request()
.post("/site/api/users")
.withJsonBody(input) // Auto-serializes to JSON
.execute();Response Status Codes with executeWithStatus():
// Get both status code and typed body
Response<User> response = brxm.request()
.post("/site/api/users")
.withJsonBody(newUser)
.executeWithStatus(User.class);
assertThat(response.status()).isEqualTo(201);
assertThat(response.isSuccessful()).isTrue();
assertThat(response.body().getName()).isEqualTo("John");
// Error response handling
Response<ErrorDto> error = brxm.request()
.get("/site/api/missing")
.executeWithStatus(ErrorDto.class);
assertThat(error.status()).isEqualTo(404);
assertThat(error.isClientError()).isTrue();Fluent Request Builder:
@BrxmJaxrsTest
public class FluentApiTest {
private DynamicJaxrsTest brxm;
@Test
void testFluentRequest() {
String response = brxm.request()
.get("/site/api/news")
.withHeader("X-Custom", "value")
.queryParam("category", "tech")
.execute();
assertTrue(response.contains("news"));
}
}Auto-Managed Repository Sessions:
@Test
void testRepositoryAccess() {
try (RepositorySession session = brxm.repository()) {
Node newsNode = session.getNode("/content/documents/news");
assertEquals("hippo:handle", newsNode.getPrimaryNodeType().getName());
}
// Session automatically closed
}Benefits:
executeAs(Class)- JSON response deserialization in one lineexecuteWithStatus(Class)- Get HTTP status code + typed body togetherwithJsonBody(String)/withJsonBody(Object)- JSON request body in one lineResponse<T>wrapper withstatus(),body(),isSuccessful(),isClientError()- Reduces boilerplate for request setup
- Auto-cleanup of JCR sessions (try-with-resources)
- Method chaining for readable test code
- Works with both PageModel and JAX-RS tests
Context-rich error messages with actionable fix suggestions:
Before:
IllegalStateException: Test instance not initialized. This should not happen.
After:
Invalid test state: Test instance not available in beforeEach
Expected: DynamicPageModelTest should be initialized in beforeAll
Actual: Instance is null
This indicates a bug in BRUT or misuse of test infrastructure.
Please report this issue with full stack trace.
Features:
- Custom Exception Types -
BrutConfigurationExceptionandComponentConfigurationExceptionwith semantic factory methods - Missing Annotation Errors - Shows import statement and example usage
- Missing Field Errors - Lists all scanned fields and their types to help identify typos
- Bootstrap Failures - Shows complete configuration context (bean patterns, Spring configs, HST root)
- Configuration Summary Logging - Detailed startup logs showing resolved configuration
- Step-by-Step Progress - ConfigServiceRepository logs each initialization step
Configuration Summary Example:
========================================
PageModel Configuration Summary
========================================
Test Class: org.example.NewsPageModelTest
Bean Patterns:
- classpath*:org/example/beans/*.class
Spring Configs:
- /org/example/custom-pagemodel.xml [AUTO-DETECTED]
HST Root: /hst:myproject
========================================
PageModel Initialization Starting
========================================
Error Context Example:
Bootstrap failed during: PageModel test initialization
Configuration attempted:
Bean patterns: classpath*:org/example/beans/*.class
Spring configs: /org/example/custom-pagemodel.xml
HST root: /hst:myproject
Root cause: FileNotFoundException: /org/example/custom-pagemodel.xml
To fix:
1. Check that all specified resources exist on classpath
2. Verify bean packages contain valid Spring components
3. Ensure Spring config files are well-formed XML
4. Check logs above for more specific error details
Benefits:
- Faster debugging with clear error context
- No more "this should not happen" messages
- Configuration visibility for troubleshooting
- Actionable fix suggestions in every error
- Field scan results show exactly what was found vs. expected
Multi-Test Support and Stability Improvements
This release focuses on enabling reliable testing with multiple test methods and improving overall framework stability.
Key Improvements:
- JUnit 4
@Beforepattern support - Component manager now properly shared across test instances while maintaining per-test isolation - Servlet context registration -
ReentrantLock-based synchronization prevents duplicateHippoWebappContextregistration when multiple test classes run sequentially - RequestContextProvider support - JAX-RS resources can now access
RequestContextProvider.get()with proper ThreadLocal management - Null-safety and error handling - Defensive checks throughout with clear error messages for initialization issues
- Exception visibility - Full stack traces logged and propagated for easier debugging
Usage:
Both JUnit 4 and JUnit 5 patterns are supported:
// JUnit 5 (Recommended)
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
public class MyTest extends AbstractJaxrsTest {
@BeforeAll public void init() { super.init(); }
@BeforeEach public void beforeEach() { setupForNewRequest(); }
}
// JUnit 4 (Fully supported)
public class MyTest extends AbstractJaxrsTest {
@Before public void setUp() { super.init(); /* custom setup */ }
}Compatibility with brXM version 16.0.0
Compatibility with brXM version 15.0.1+
Compatibility with brXM version 15.0.0
Compatibility with brXM version 14.0.0-2
Compatibility with brXM version 13.4.0
- Fixed breaking changes coming from brXM due to dynamic beans feature. Dynamic beans are not supported in brut.
- Subclasses of SimpleComponentTest in brut-components can now provide their own SpringComponentManager
- Fixed a bug in brut-resources where servletContext was null in SpringComponentManager (dynamic beans regression)
Release date: 30 March 2019
- Upgrade to brXM 13
Release date: 30 March 2019
- Apply Bloomreach Forge best practices and publish it on the Forge, under different Maven coordinates of the artifacts.
- Available for brXM 12.x (developed and tested on 12.6.0)
- Older release with different group id