[ 
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)

Reply via email to