Serge Huber created UNOMI-920:
---------------------------------

             Summary: 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
             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