[
https://issues.apache.org/jira/browse/UNOMI-920?page=com.atlassian.jira.plugin.system.issuetabpanels:all-tabpanel
]
Serge Huber reassigned UNOMI-920:
---------------------------------
Assignee: Serge Huber
> Improve toString() Methods with YAML Formatting for Better Debugging
> --------------------------------------------------------------------
>
> Key: UNOMI-920
> URL: https://issues.apache.org/jira/browse/UNOMI-920
> Project: Apache Unomi
> Issue Type: Bug
> Components: unomi(-core)
> Affects Versions: unomi-3.0.0, unomi-3.1.0
> Reporter: Serge Huber
> Assignee: Serge Huber
> Priority: Major
> Fix For: unomi-3.1.0
>
>
> h2. Problem Statement
> Many core API classes in Unomi currently have poor or missing {{toString()}}
> implementations, making debugging and logging difficult. When these objects
> are logged or inspected, the output is either:
> * Missing entirely (uses {{Object.toString()}})
> * Hard to read (StringBuilder-based output with nested structures)
> * Incomplete (inherited implementations that only show metadata, not nested
> objects)
> This is particularly problematic for complex objects with nested structures
> like:
> * {{Profile}} with nested {{Consent}} objects, maps, and sets
> * {{Event}} with nested {{Profile}}, {{Session}}, and complex property maps
> * {{ContextRequest}}/{{ContextResponse}} with multiple nested lists and maps
> * {{Rule}}/{{Segment}}/{{Campaign}} with nested {{Condition}} objects
> h2. Proposed Solution
> Implement YAML-formatted {{toString()}} methods for all complex API classes,
> following a consistent pattern that:
> * Uses a lightweight YAML formatter (SnakeYaml) for readable output
> * Handles circular references properly (e.g., Profile ↔ Event ↔ Session)
> * Shows nested structures clearly with proper indentation
> * Provides a fluent API for building YAML maps
> h2. Proposed Pattern (Example Implementation)
> The following pattern should be implemented first for Condition and
> ConditionType classes, which will serve as reference implementations:
> {code:java}
> public class Condition {
> public Map<String, Object> toYaml(Set<Object> visited) {
> // Use IdentityHashMap for visited set to ensure identity-based
> comparison
> if (visited.contains(this)) {
> return YamlUtils.circularRef(); // Returns {"$ref": "circular"}
> }
> visited.add(this);
> try {
> return YamlUtils.YamlMapBuilder.create()
> .put("type", conditionTypeId)
> .putIfNotEmpty("parameters", parameterValues)
> .putIfNotNull("nestedCondition", nestedCondition != null ?
> nestedCondition.toYaml(visited) : null) // Pass visited
> set
> .build();
> } finally {
> visited.remove(this); // Clean up to allow same object in
> different branches
> }
> }
>
> @Override
> public String toString() {
> // Use IdentityHashMap for proper identity-based comparison
> return YamlUtils.format(toYaml(Collections.newSetFromMap(new
> IdentityHashMap<>())));
> }
> }
> {code}
> Key aspects of this pattern:
> * {{toYaml(Set<Object> visited)}} method returns {{Map<String, Object>}} for
> conversion
> * Uses {{IdentityHashMap}}-based visited set for circular reference detection
> (identity-based, not equality-based)
> * Uses {{YamlUtils.YamlMapBuilder}} fluent API for conditional field inclusion
> * Try/finally ensures visited set cleanup and allows same object in different
> branches
> * Passes visited set to nested object's {{toYaml()}} method
> * {{toString()}} delegates to {{YamlUtils.format()}} with
> IdentityHashMap-based visited set
> *Note:* Condition and ConditionType should be implemented first in Phase 1 to
> establish this pattern, which will then be followed for all other classes.
> h2. Classes That Would Benefit
> h3. High Priority (Critical - No toString())
> * *Condition* - Core class used throughout Unomi, has nested ConditionType
> and recursive Condition structures. This should be implemented first as it
> serves as the reference pattern for other classes.
> * *ConditionType* - Defines condition structure, has nested Parameter and
> ConditionValidation objects. Should be implemented alongside Condition as
> they are closely related.
> * *Event* - Frequently logged/debugged, has nested Profile, Session, Item
> objects
> * *ContextRequest* - API request class with nested Event lists,
> PersonalizedContent, Profile
> * *ContextResponse* - API response class with multiple maps, sets, and
> TraceNode
> * *Tenant* - Complex configuration with nested ResourceQuota and ApiKey lists
> * *Session* - Frequently debugged, has nested Profile
> * *Action* - Similar structure to Condition, used in Rules
> * *TraceNode* - Recursive structure (List<TraceNode> children), used in
> ContextResponse
> h3. Medium Priority (Poor toString() - Inherits Basic Implementation)
> * *Profile* - Has StringBuilder-based toString() but nested structures are
> hard to read
> * *Rule* - Inherits MetadataItem.toString(), doesn't show Condition or Actions
> * *Segment* - Inherits MetadataItem.toString(), doesn't show Condition
> * *Campaign* - Inherits MetadataItem.toString(), doesn't show entryCondition
> or dates
> * *Goal* - Inherits MetadataItem.toString(), doesn't show
> startEvent/targetEvent Conditions
> * *ActionType* - Inherits MetadataItem.toString(), doesn't show parameters
> h3. Lower Priority (Nested Classes and Supporting Classes)
> * *Query* - Has nested Condition
> * *BatchUpdate* - Has nested Condition
> * *Patch* - Has complex data field
> * *EventsCollectorRequest* - Has List<Event>
> * *PersonalizationService.PersonalizedContent* - Inner class, nested in
> ContextRequest
> * *PersonalizationService.PersonalizationRequest* - Inner class, nested in
> ContextRequest
> * *PersonalizationService.Filter* - Inner class, has nested Condition
> * *PersonalizationService.Target* - Simple inner class
> * *PersonalizationResult* - Used in ContextResponse
> * *ApiKey* - Used in Tenant
> * *ResourceQuota* - Used in Tenant
> * *Consent* - Has basic toString() but could be improved for consistency
> h2. Implementation Notes
> h3. Infrastructure
> The following utilities would be needed (or already exist as examples):
> * {{YamlUtils}} - Wrapper around SnakeYaml with fluent API
> * {{YamlUtils.YamlMapBuilder}} - Fluent builder for conditional map
> construction
> * {{YamlUtils.toYamlValue()}} - Recursive value conversion
> * {{YamlUtils.circularRef()}} - Creates circular reference markers
> h3. Circular Reference Handling
> Classes with bidirectional references need special handling:
> * Profile ↔ Event ↔ Session (bidirectional references)
> * TraceNode has recursive structure: List<TraceNode> children (parent-child
> relationships)
> *Solution Pattern:*
> {code:java}
> public Map<String, Object> toYaml(Set<Object> visited) {
> // Use IdentityHashMap for visited set to ensure identity-based comparison
> if (visited.contains(this)) {
> return YamlUtils.circularRef(); // Returns {"$ref": "circular"}
> }
> visited.add(this);
> try {
> return YamlUtils.YamlMapBuilder.create()
> .put("field1", value1)
> .put("nestedObject", nestedObject != null ?
> nestedObject.toYaml(visited) : null)
> .build();
> } finally {
> visited.remove(this); // Clean up to allow same object in different
> branches
> }
> }
> @Override
> public String toString() {
> // Use IdentityHashMap for proper identity-based comparison
> return YamlUtils.format(toYaml(Collections.newSetFromMap(new
> IdentityHashMap<>())));
> }
> {code}
> *Key Implementation Points:*
> * *IdentityHashMap for visited sets* - Critical for proper identity
> comparison (not equality-based). Regular HashSet uses {{equals()}} which may
> not detect cycles correctly for objects with custom equality.
> * *Cycle detection* - Check {{visited.contains(this)}} at the start of
> {{toYaml()}} method
> * *Try/finally cleanup* - Ensures visited set is cleaned up even if
> exceptions occur, and allows same object to appear in different branches of
> the object graph
> * *Circular reference marker* - Return {{YamlUtils.circularRef()}} when cycle
> detected (produces {{"$ref": "circular"}} in YAML output)
> * *Pass visited set* - Always pass the visited set to nested object's
> {{toYaml()}} method when traversing object graphs
> *Special Cases:*
> * *Profile ↔ Event ↔ Session:* Each class should use {{Set<Object>}} with
> IdentityHashMap to handle heterogeneous object graphs
> * *TraceNode recursive structure:* Use {{Set<TraceNode>}} with
> IdentityHashMap for children traversal, or {{Set<Object>}} if mixing with
> other types
> * *Mixed object graphs:* Use {{Set<Object>}} with IdentityHashMap when
> traversing heterogeneous object graphs (most common case)
> *Example for Event with Profile/Session references:*
> {code:java}
> public Map<String, Object> toYaml(Set<Object> visited) {
> if (visited.contains(this)) {
> return YamlUtils.circularRef();
> }
> visited.add(this);
> try {
> return YamlUtils.YamlMapBuilder.create()
> .put("itemId", itemId)
> .put("eventType", eventType)
> .put("properties", properties)
> .putIfNotNull("profile", profile != null ?
> profile.toYaml(visited) : null) // Pass visited set
> .putIfNotNull("session", session != null ?
> session.toYaml(visited) : null) // Pass visited set
> .putIfNotNull("source", source != null ?
> source.toYaml(visited) : null) // Pass visited set
> .build();
> } finally {
> visited.remove(this);
> }
> }
> {code}
> *Why IdentityHashMap?*
> Regular {{HashSet}} uses {{equals()}} and {{hashCode()}} for comparison. For
> objects with custom equality (like many Unomi classes), two different object
> instances might be considered "equal" but are actually different objects in
> memory. This can cause:
> * False cycle detection (thinks it's seen an object when it hasn't)
> * Missing cycle detection (doesn't detect actual cycles)
> {{IdentityHashMap}} uses object identity ({{==}}) for comparison, ensuring we
> track actual object instances, not just "equal" objects. This is critical for
> proper cycle detection in object graphs.
> h3. Inheritance Considerations
> * Classes extending {{Item}} should include Item fields (itemId, itemType,
> scope, version, systemMetadata, audit fields)
> * Classes extending {{MetadataItem}} should include metadata fields
> * Consider helper methods in base classes if many classes need similar
> handling
> h2. Benefits
> * *Improved Debugging* - Logs and stack traces will show readable YAML
> instead of object references
> * *Better Developer Experience* - Developers can easily inspect complex
> objects during development
> * *Consistency* - All complex objects follow the same output format
> * *Copy-Paste Friendly* - YAML output can be easily copied and reformatted
> for documentation or testing
> h2. Implementation Phases
> h3. Phase 1: Critical Classes (No toString())
> Implement for: Condition, ConditionType (reference implementations), Event,
> ContextRequest, ContextResponse, Tenant, Session, Action, TraceNode
> *Note:* Condition and ConditionType should be implemented first as they
> establish the pattern and serve as reference implementations for other
> classes.
> h3. Phase 2: Poor toString() Classes
> Replace existing implementations for: Profile, Rule, Segment, Campaign, Goal,
> ActionType
> h3. Phase 3: Nested Classes
> Implement for PersonalizationService inner classes when implementing
> ContextRequest
> h3. Phase 4: Supporting Classes
> Implement for Query, BatchUpdate, Patch, EventsCollectorRequest, and other
> supporting classes
> h2. Example Output
> Instead of:
> {code}
> org.apache.unomi.api.Profile@12345678
> {code}
> Or:
> {code}
> Profile{itemId=profile-123, properties={key1=value1, key2=value2}, ...}
> {code}
> We would get:
> {code:yaml}
> itemId: profile-123
> itemType: profile
> scope: systemscope
> properties:
> key1: value1
> key2: value2
> segments:
> - segment1
> - segment2
> consents:
> consent1:
> scope: systemscope
> typeIdentifier: type1
> status: GRANTED
> {code}
> When circular references are detected, they are marked clearly:
> {code:yaml}
> itemId: event-123
> eventType: view
> profile:
> itemId: profile-123
> # ... profile fields ...
> # If this profile references back to the event, it would show:
> session:
> itemId: session-123
> profile:
> $ref: circular # Circular reference marker
> {code}
> h2. Acceptance Criteria
> * All high-priority classes have YAML-based {{toString()}} implementations
> * Circular references are handled correctly (no infinite loops)
> * Output is readable and properly formatted YAML
> * Implementation follows the established pattern (toYaml() method +
> toString() delegation)
> * No performance regression in logging/debugging scenarios
> * Unit tests verify YAML output format for key classes
> h2. Related Work
> This improvement would complement existing work on improving condition
> evaluation and debugging capabilities. Condition and ConditionType should be
> implemented first in Phase 1 to establish the pattern and serve as reference
> implementations for other classes.
--
This message was sent by Atlassian Jira
(v8.20.10#820010)