This is an automated email from the ASF dual-hosted git repository.
shuber pushed a commit to branch unomi-3-dev
in repository https://gitbox.apache.org/repos/asf/unomi.git
The following commit(s) were added to refs/heads/unomi-3-dev by this push:
new a779bf7f7 UNOMI-920 Add safe string conversion utility with length
checks in `DefaultRequestTracer`, integrate YAML serialization capabilities
across core entities, and test circular reference detection.
a779bf7f7 is described below
commit a779bf7f7f42c61638e81d9f0cd4f32ba9aea6b0
Author: Serge Huber <[email protected]>
AuthorDate: Thu Jan 1 16:20:40 2026 +0100
UNOMI-920 Add safe string conversion utility with length checks in
`DefaultRequestTracer`, integrate YAML serialization capabilities across core
entities, and test circular reference detection.
---
api/pom.xml | 4 +
api/src/main/java/org/apache/unomi/api/Item.java | 68 +++-
.../main/java/org/apache/unomi/api/Metadata.java | 56 +++-
.../java/org/apache/unomi/api/MetadataItem.java | 50 ++-
.../main/java/org/apache/unomi/api/Parameter.java | 40 ++-
.../java/org/apache/unomi/api/actions/Action.java | 48 ++-
.../org/apache/unomi/api/actions/ActionType.java | 42 ++-
.../org/apache/unomi/api/conditions/Condition.java | 96 ++++--
.../apache/unomi/api/conditions/ConditionType.java | 50 ++-
.../unomi/api/conditions/ConditionValidation.java | 10 +-
.../main/java/org/apache/unomi/api/goals/Goal.java | 44 ++-
.../main/java/org/apache/unomi/api/rules/Rule.java | 46 ++-
.../org/apache/unomi/api/segments/Scoring.java | 38 ++-
.../apache/unomi/api/segments/ScoringElement.java | 47 ++-
.../org/apache/unomi/api/segments/Segment.java | 40 ++-
.../java/org/apache/unomi/api/utils/YamlUtils.java | 94 +++++-
.../org/apache/unomi/api/utils/YamlUtilsTest.java | 357 ++++++++++++++++++++-
.../unomi/tracing/impl/DefaultRequestTracer.java | 29 +-
.../tracing/impl/DefaultTracerServiceTest.java | 125 +++++---
19 files changed, 1125 insertions(+), 159 deletions(-)
diff --git a/api/pom.xml b/api/pom.xml
index 5b0072b5f..63f48d7b4 100644
--- a/api/pom.xml
+++ b/api/pom.xml
@@ -88,6 +88,10 @@
<artifactId>osgi.core</artifactId>
<scope>provided</scope>
</dependency>
+ <dependency>
+ <groupId>org.yaml</groupId>
+ <artifactId>snakeyaml</artifactId>
+ </dependency>
<!-- Test Dependencies -->
<dependency>
diff --git a/api/src/main/java/org/apache/unomi/api/Item.java
b/api/src/main/java/org/apache/unomi/api/Item.java
index 3e591fb74..44da45f5c 100644
--- a/api/src/main/java/org/apache/unomi/api/Item.java
+++ b/api/src/main/java/org/apache/unomi/api/Item.java
@@ -17,15 +17,18 @@
package org.apache.unomi.api;
+import org.apache.unomi.api.utils.YamlUtils;
+import org.apache.unomi.api.utils.YamlUtils.YamlConvertible;
+import org.apache.unomi.api.utils.YamlUtils.YamlMapBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.Serializable;
-import java.util.Date;
-import java.util.HashMap;
-import java.util.Map;
+import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
+import static org.apache.unomi.api.utils.YamlUtils.toYamlValue;
+
/**
* A context server tracked entity. All tracked entities need to extend this
class so as to provide the minimal information the context server needs to be
able to track such
* entities and operate on them. Items are persisted according to their type
(structure) and identifier (identity). Of note, all Item subclasses
<strong>must</strong> define a
@@ -37,7 +40,7 @@ import java.util.concurrent.ConcurrentHashMap;
* though scopes could span across sites depending on the desired analysis
granularity). Scopes allow clients accessing the context server to filter data.
The context server
* defines a built-in scope ({@link Metadata#SYSTEM_SCOPE}) that clients can
use to share data across scopes.
*/
-public abstract class Item implements Serializable {
+public abstract class Item implements Serializable, YamlConvertible {
private static final Logger LOGGER =
LoggerFactory.getLogger(Item.class.getName());
private static final long serialVersionUID = 1217180125083162915L;
@@ -222,4 +225,61 @@ public abstract class Item implements Serializable {
public void setLastSyncDate(Date lastSyncDate) {
this.lastSyncDate = lastSyncDate;
}
+
+ /**
+ * Converts this item to a Map structure for YAML output.
+ * Implements YamlConvertible interface with circular reference detection.
+ *
+ * @param visited set of already visited objects to prevent infinite
recursion (may be null)
+ * @return a Map representation of this item
+ */
+ @Override
+ public Map<String, Object> toYaml(Set<Object> visited, int maxDepth) {
+ if (maxDepth <= 0) {
+ return YamlMapBuilder.create()
+ .put("itemId", itemId)
+ .put("itemType", itemType)
+ .put("systemMetadata", "<max depth exceeded>")
+ .build();
+ }
+ final Set<Object> visitedSet = visited != null ? visited : new
HashSet<>();
+ // Check if already visited - if so, we're being called from a child
class via super.toYaml()
+ // OR it's a real circular reference. We can't distinguish, but since
child classes
+ // (like Rule, ConditionType, etc.) all check for circular refs before
calling super,
+ // if we're already visited here, it's safe to assume it's a super
call, not a circular ref.
+ // If Item is directly serialized and encounters itself, the check
would happen at the
+ // top level before nested processing, so this should be safe.
+ boolean alreadyVisited = visitedSet.contains(this);
+ if (!alreadyVisited) {
+ // First time seeing this object - add it to track for circular
references
+ visitedSet.add(this);
+ }
+ try {
+ return YamlMapBuilder.create()
+ .put("itemId", itemId) // Always include, even if null, to
reflect actual state
+ .put("itemType", itemType) // Always include, even if null,
to reflect actual state
+ .putIfNotNull("scope", scope)
+ .putIfNotNull("version", version)
+ .putIfNotNull("systemMetadata", systemMetadata != null &&
!systemMetadata.isEmpty() ? toYamlValue(systemMetadata, visitedSet, maxDepth -
1) : null)
+ .putIfNotNull("tenantId", tenantId)
+ .putIfNotNull("createdBy", createdBy)
+ .putIfNotNull("lastModifiedBy", lastModifiedBy)
+ .putIfNotNull("creationDate", creationDate)
+ .putIfNotNull("lastModificationDate", lastModificationDate)
+ .putIfNotNull("sourceInstanceId", sourceInstanceId)
+ .putIfNotNull("lastSyncDate", lastSyncDate)
+ .build();
+ } finally {
+ // Only remove if we added it (i.e., if it wasn't already visited)
+ if (!alreadyVisited) {
+ visitedSet.remove(this);
+ }
+ }
+ }
+
+ @Override
+ public String toString() {
+ Map<String, Object> map = toYaml();
+ return YamlUtils.format(map);
+ }
}
diff --git a/api/src/main/java/org/apache/unomi/api/Metadata.java
b/api/src/main/java/org/apache/unomi/api/Metadata.java
index 7e0f8da8d..ec25c7fa8 100644
--- a/api/src/main/java/org/apache/unomi/api/Metadata.java
+++ b/api/src/main/java/org/apache/unomi/api/Metadata.java
@@ -17,16 +17,24 @@
package org.apache.unomi.api;
+import org.apache.unomi.api.utils.YamlUtils;
+import org.apache.unomi.api.utils.YamlUtils.YamlConvertible;
+import org.apache.unomi.api.utils.YamlUtils.YamlMapBuilder;
+
import java.io.Serializable;
+import java.util.HashSet;
import java.util.LinkedHashSet;
+import java.util.Map;
import java.util.Set;
+import static org.apache.unomi.api.utils.YamlUtils.circularRef;
+
/**
* A class providing information about context server entities.
*
* @see MetadataItem
*/
-public class Metadata implements Comparable<Metadata>, Serializable {
+public class Metadata implements Comparable<Metadata>, Serializable,
YamlConvertible {
private static final long serialVersionUID = 7446061538573517071L;
@@ -279,19 +287,41 @@ public class Metadata implements Comparable<Metadata>,
Serializable {
return result;
}
+ /**
+ * Converts this metadata to a Map structure for YAML output.
+ * Implements YamlConvertible interface with circular reference detection.
+ *
+ * @param visited set of already visited objects to prevent infinite
recursion (may be null)
+ * @return a Map representation of this metadata
+ */
+ @Override
+ public Map<String, Object> toYaml(Set<Object> visited, int maxDepth) {
+ if (visited != null && visited.contains(this)) {
+ return circularRef();
+ }
+ final Set<Object> visitedSet = visited != null ? visited : new
HashSet<>();
+ visitedSet.add(this);
+ try {
+ return YamlMapBuilder.create()
+ .putIfNotNull("id", id)
+ .putIfNotNull("name", name)
+ .putIfNotNull("description", description)
+ .putIfNotNull("scope", scope)
+ .putIfNotEmpty("tags", tags)
+ .putIfNotEmpty("systemTags", systemTags)
+ .putIf("enabled", true, enabled)
+ .putIf("missingPlugins", true, missingPlugins)
+ .putIf("hidden", true, hidden)
+ .putIf("readOnly", true, readOnly)
+ .build();
+ } finally {
+ visitedSet.remove(this);
+ }
+ }
+
@Override
public String toString() {
- return "Metadata{" +
- "id='" + id + '\'' +
- ", name='" + name + '\'' +
- ", description='" + description + '\'' +
- ", scope='" + scope + '\'' +
- ", tags=" + tags +
- ", systemTags=" + systemTags +
- ", enabled=" + enabled +
- ", missingPlugins=" + missingPlugins +
- ", hidden=" + hidden +
- ", readOnly=" + readOnly +
- '}';
+ Map<String, Object> map = toYaml();
+ return YamlUtils.format(map);
}
}
diff --git a/api/src/main/java/org/apache/unomi/api/MetadataItem.java
b/api/src/main/java/org/apache/unomi/api/MetadataItem.java
index 6a23ea9d9..35ef596a2 100644
--- a/api/src/main/java/org/apache/unomi/api/MetadataItem.java
+++ b/api/src/main/java/org/apache/unomi/api/MetadataItem.java
@@ -17,8 +17,16 @@
package org.apache.unomi.api;
+import org.apache.unomi.api.utils.YamlUtils;
+import org.apache.unomi.api.utils.YamlUtils.YamlMapBuilder;
+
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlTransient;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+import static org.apache.unomi.api.utils.YamlUtils.toYamlValue;
/**
* A superclass for all {@link Item}s that bear {@link Metadata}.
@@ -60,10 +68,46 @@ public abstract class MetadataItem extends Item {
return scope;
}
+ /**
+ * Converts this metadata item to a Map structure for YAML output.
+ * Merges fields from Item parent class and adds metadata field.
+ * Subclasses should override this method, call super.toYaml(visited), and
add their specific fields.
+ *
+ * @param visited set of already visited objects to prevent infinite
recursion (may be null)
+ * @return a Map representation of this metadata item
+ */
+ @Override
+ public Map<String, Object> toYaml(Set<Object> visited, int maxDepth) {
+ if (maxDepth <= 0) {
+ return YamlMapBuilder.create()
+ .put("metadata", "<max depth exceeded>")
+ .build();
+ }
+ final Set<Object> visitedSet = visited != null ? visited : new
HashSet<>();
+ // Check if already visited - if so, we're being called from a child
class via super.toYaml()
+ // In that case, skip the circular reference check and just proceed
+ boolean alreadyVisited = visitedSet.contains(this);
+ if (!alreadyVisited) {
+ // Only check for circular references if this is the first time
we're seeing this object
+ visitedSet.add(this);
+ }
+ try {
+ return YamlMapBuilder.create()
+ .mergeObject(super.toYaml(visitedSet, maxDepth))
+ .putIfNotNull("metadata", metadata != null ?
toYamlValue(metadata, visitedSet, maxDepth - 1) : null)
+ .build();
+ } finally {
+ // Only remove if we added it (i.e., if it wasn't already visited)
+ if (!alreadyVisited) {
+ visitedSet.remove(this);
+ }
+ }
+ }
+
+
@Override
public String toString() {
- return "MetadataItem{" +
- "metadata=" + metadata +
- '}';
+ Map<String, Object> map = toYaml();
+ return YamlUtils.format(map);
}
}
diff --git a/api/src/main/java/org/apache/unomi/api/Parameter.java
b/api/src/main/java/org/apache/unomi/api/Parameter.java
index 7155472f7..0e24a2e21 100644
--- a/api/src/main/java/org/apache/unomi/api/Parameter.java
+++ b/api/src/main/java/org/apache/unomi/api/Parameter.java
@@ -18,14 +18,20 @@
package org.apache.unomi.api;
import org.apache.unomi.api.conditions.ConditionValidation;
+import org.apache.unomi.api.utils.YamlUtils;
+import org.apache.unomi.api.utils.YamlUtils.YamlConvertible;
import java.io.Serializable;
+import java.util.Map;
+import java.util.Set;
+
+import static org.apache.unomi.api.utils.YamlUtils.toYamlValue;
/**
* A representation of a condition parameter, to be used in the segment
building UI to either select parameters from a
* choicelist or to enter a specific value.
*/
-public class Parameter implements Serializable {
+public class Parameter implements Serializable, YamlConvertible {
private static final long serialVersionUID = 6019392686888941547L;
@@ -93,14 +99,32 @@ public class Parameter implements Serializable {
this.validation = validation;
}
+ /**
+ * Converts this parameter to a Map structure for YAML output.
+ * Implements YamlConvertible interface.
+ *
+ * @param visited set of already visited objects to prevent infinite
recursion (may be null)
+ * @return a Map representation of this parameter
+ */
+ @Override
+ public Map<String, Object> toYaml(Set<Object> visited, int maxDepth) {
+ if (maxDepth <= 0) {
+ return YamlUtils.YamlMapBuilder.create()
+ .put("id", id)
+ .put("validation", "<max depth exceeded>")
+ .build();
+ }
+ return YamlUtils.YamlMapBuilder.create()
+ .putIfNotNull("id", id)
+ .putIfNotNull("type", type)
+ .putIf("multivalued", true, multivalued)
+ .putIfNotNull("defaultValue", defaultValue)
+ .putIfNotNull("validation", validation != null ?
toYamlValue(validation, visited, maxDepth - 1) : null)
+ .build();
+ }
+
@Override
public String toString() {
- return "Parameter{" +
- "id='" + id + '\'' +
- ", type='" + type + '\'' +
- ", multivalued=" + multivalued +
- ", defaultValue=" + defaultValue +
- ", validation=" + validation +
- '}';
+ return YamlUtils.format(toYaml());
}
}
diff --git a/api/src/main/java/org/apache/unomi/api/actions/Action.java
b/api/src/main/java/org/apache/unomi/api/actions/Action.java
index b3505bbe8..29c6927e1 100644
--- a/api/src/main/java/org/apache/unomi/api/actions/Action.java
+++ b/api/src/main/java/org/apache/unomi/api/actions/Action.java
@@ -18,18 +18,26 @@
package org.apache.unomi.api.actions;
import org.apache.unomi.api.rules.Rule;
+import org.apache.unomi.api.utils.YamlUtils;
+import org.apache.unomi.api.utils.YamlUtils.YamlConvertible;
+import org.apache.unomi.api.utils.YamlUtils.YamlMapBuilder;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlTransient;
import java.io.Serializable;
import java.util.HashMap;
+import java.util.HashSet;
import java.util.Map;
+import java.util.Set;
+
+import static org.apache.unomi.api.utils.YamlUtils.circularRef;
+import static org.apache.unomi.api.utils.YamlUtils.toYamlValue;
/**
* An action that can be executed as a consequence of a {@link Rule} being
triggered. An action is characterized by its associated {@link
* ActionType} and parameter values.
*/
-public class Action implements Serializable {
+public class Action implements Serializable, YamlConvertible {
private ActionType actionType;
private String actionTypeId;
private Map<String, Object> parameterValues = new HashMap<>();
@@ -117,4 +125,42 @@ public class Action implements Serializable {
parameterValues.put(name, value);
}
+ /**
+ * Converts this action to a Map structure for YAML output.
+ * Implements YamlConvertible interface with circular reference detection.
+ *
+ * @param visited set of already visited objects to prevent infinite
recursion (may be null)
+ * @return a Map representation of this action
+ */
+ @Override
+ public Map<String, Object> toYaml(Set<Object> visited, int maxDepth) {
+ if (maxDepth <= 0) {
+ return YamlMapBuilder.create()
+ .put("type", actionTypeId != null ? actionTypeId : "Action")
+ .put("parameterValues", "<max depth exceeded>")
+ .build();
+ }
+ if (visited != null && visited.contains(this)) {
+ return circularRef();
+ }
+ final Set<Object> visitedSet = visited != null ? visited : new
HashSet<>();
+ visitedSet.add(this);
+ try {
+ YamlMapBuilder builder = YamlMapBuilder.create()
+ .put("type", actionTypeId != null ? actionTypeId : "Action");
+ if (parameterValues != null && !parameterValues.isEmpty()) {
+ builder.put("parameterValues", toYamlValue(parameterValues,
visitedSet, maxDepth - 1));
+ }
+ return builder.build();
+ } finally {
+ visitedSet.remove(this);
+ }
+ }
+
+ @Override
+ public String toString() {
+ Map<String, Object> map = toYaml();
+ return YamlUtils.format(map);
+ }
+
}
diff --git a/api/src/main/java/org/apache/unomi/api/actions/ActionType.java
b/api/src/main/java/org/apache/unomi/api/actions/ActionType.java
index fddf732f9..03c4e6e0b 100644
--- a/api/src/main/java/org/apache/unomi/api/actions/ActionType.java
+++ b/api/src/main/java/org/apache/unomi/api/actions/ActionType.java
@@ -21,14 +21,18 @@ import org.apache.unomi.api.Metadata;
import org.apache.unomi.api.MetadataItem;
import org.apache.unomi.api.Parameter;
import org.apache.unomi.api.PluginType;
+import org.apache.unomi.api.utils.YamlUtils.YamlConvertible;
+import org.apache.unomi.api.utils.YamlUtils.YamlMapBuilder;
-import java.util.ArrayList;
-import java.util.List;
+import java.util.*;
+
+import static org.apache.unomi.api.utils.YamlUtils.circularRef;
+import static org.apache.unomi.api.utils.YamlUtils.toYamlValue;
/**
* A type definition for {@link Action}s.
*/
-public class ActionType extends MetadataItem implements PluginType {
+public class ActionType extends MetadataItem implements PluginType,
YamlConvertible {
public static final String ITEM_TYPE = "actionType";
private static final long serialVersionUID = -3522958600710010935L;
@@ -113,4 +117,36 @@ public class ActionType extends MetadataItem implements
PluginType {
public void setPluginId(long pluginId) {
this.pluginId = pluginId;
}
+
+ /**
+ * Converts this action type to a Map structure for YAML output.
+ * Implements YamlConvertible interface with circular reference detection.
+ *
+ * @param visited set of already visited objects to prevent infinite
recursion (may be null)
+ * @return a Map representation of this action type
+ */
+ @Override
+ public Map<String, Object> toYaml(Set<Object> visited, int maxDepth) {
+ if (maxDepth <= 0) {
+ return YamlMapBuilder.create()
+ .put("parameters", "<max depth exceeded>")
+ .put("pluginId", pluginId)
+ .build();
+ }
+ if (visited != null && visited.contains(this)) {
+ return circularRef();
+ }
+ final Set<Object> visitedSet = visited != null ? visited : new
HashSet<>();
+ visitedSet.add(this);
+ try {
+ return YamlMapBuilder.create()
+ .mergeObject(super.toYaml(visitedSet, maxDepth))
+ .putIfNotNull("actionExecutor", actionExecutor)
+ .putIfNotEmpty("parameters", parameters != null ?
(Collection<?>) toYamlValue(parameters, visitedSet, maxDepth - 1) : null)
+ .put("pluginId", pluginId)
+ .build();
+ } finally {
+ visitedSet.remove(this);
+ }
+ }
}
diff --git a/api/src/main/java/org/apache/unomi/api/conditions/Condition.java
b/api/src/main/java/org/apache/unomi/api/conditions/Condition.java
index d41890bff..812ee5862 100644
--- a/api/src/main/java/org/apache/unomi/api/conditions/Condition.java
+++ b/api/src/main/java/org/apache/unomi/api/conditions/Condition.java
@@ -18,23 +18,21 @@
package org.apache.unomi.api.conditions;
import org.apache.unomi.api.utils.YamlUtils;
+import org.apache.unomi.api.utils.YamlUtils.YamlConvertible;
import org.apache.unomi.api.utils.YamlUtils.YamlMapBuilder;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlTransient;
import java.io.Serializable;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.Map;
-import java.util.Set;
-import java.util.Collections;
+import java.util.*;
import static org.apache.unomi.api.utils.YamlUtils.circularRef;
+import static org.apache.unomi.api.utils.YamlUtils.toYamlValue;
/**
* A set of elements that can be evaluated.
*/
-public class Condition implements Serializable {
+public class Condition implements Serializable, YamlConvertible {
private static final long serialVersionUID = 7584522402785053206L;
ConditionType conditionType;
@@ -133,7 +131,7 @@ public class Condition implements Serializable {
* @return the value of the specified parameter or {@code null} if no such
parameter exists
*/
public Object getParameter(String name) {
- return parameterValues.get(name);
+ return parameterValues != null ? parameterValues.get(name) : null;
}
/**
@@ -167,41 +165,95 @@ public class Condition implements Serializable {
}
/**
- * Converts this condition to a Map structure for YAML output.
+ * Converts this condition to a Map structure for YAML output with depth
limiting.
+ * Implements YamlConvertible interface with circular reference detection
and depth limiting
+ * to prevent StackOverflowError from extremely deep nested structures.
*
- * @param visited set of already visited conditions to prevent infinite
recursion
+ * @param visited set of already visited objects to prevent infinite
recursion (may be null)
+ * @param maxDepth maximum recursion depth (prevents StackOverflowError
from deep nesting)
* @return a Map representation of this condition
*/
- public Map<String, Object> toYaml(Set<Condition> visited) {
- if (visited.contains(this)) {
+ @Override
+ public Map<String, Object> toYaml(Set<Object> visited, int maxDepth) {
+ if (maxDepth <= 0) {
+ return YamlMapBuilder.create()
+ .put("type", conditionTypeId != null ? conditionTypeId :
"Condition")
+ .put("parameterValues", "<max depth exceeded>")
+ .build();
+ }
+ if (visited != null && visited.contains(this)) {
return circularRef();
}
- visited.add(this);
+ final Set<Object> visitedSet = visited != null ? visited : new
HashSet<>();
+ visitedSet.add(this);
try {
YamlMapBuilder builder = YamlMapBuilder.create()
.put("type", conditionTypeId != null ? conditionTypeId :
"Condition");
if (parameterValues != null && !parameterValues.isEmpty()) {
- parameterValues.forEach((name, value) ->
- builder.put(name, toYamlValue(value, visited)));
+ builder.put("parameterValues", toYamlValue(parameterValues,
visitedSet, maxDepth - 1));
}
return builder.build();
} finally {
- visited.remove(this);
+ visitedSet.remove(this);
}
}
- private Object toYamlValue(Object value, Set<Condition> visited) {
- if (value instanceof Condition) {
- return ((Condition) value).toYaml(visited);
+ /**
+ * Creates a deep copy of this condition, including all nested conditions
in parameter values.
+ * Recursively copies all nested conditions to avoid sharing references.
+ *
+ * @return a deep copy of this condition
+ */
+ public Condition deepCopy() {
+ Condition copied = new Condition();
+ if (this.conditionType != null) {
+ copied.setConditionType(this.conditionType);
+ } else if (this.conditionTypeId != null) {
+ copied.setConditionTypeId(this.conditionTypeId);
}
- // For non-Condition values, use empty visited set since
YamlUtils.toYamlValue
- // doesn't currently use it for circular reference detection
- return YamlUtils.toYamlValue(value, Collections.emptySet());
+
+ // Deep copy parameter values
+ Map<String, Object> copiedParams = new HashMap<>();
+ if (this.parameterValues != null) {
+ for (Map.Entry<String, Object> entry :
this.parameterValues.entrySet()) {
+ Object value = entry.getValue();
+ if (value instanceof Condition) {
+ // Recursively deep copy nested condition
+ copiedParams.put(entry.getKey(), ((Condition)
value).deepCopy());
+ } else if (value instanceof Collection) {
+ // Deep copy collection - preserve the collection type if
possible
+ Collection<?> collection = (Collection<?>) value;
+ Collection<Object> copiedCollection;
+ if (collection instanceof List) {
+ copiedCollection = new ArrayList<>();
+ } else {
+ // Fallback to ArrayList for other collection types
+ copiedCollection = new ArrayList<>();
+ }
+ for (Object item : collection) {
+ if (item instanceof Condition) {
+ // Recursively deep copy nested condition
+ copiedCollection.add(((Condition)
item).deepCopy());
+ } else {
+ // Not a condition, add as-is (for non-condition
values in collections)
+ copiedCollection.add(item);
+ }
+ }
+ copiedParams.put(entry.getKey(), copiedCollection);
+ } else {
+ // Primitive or other non-condition value, copy as-is
+ copiedParams.put(entry.getKey(), value);
+ }
+ }
+ }
+ copied.setParameterValues(copiedParams);
+
+ return copied;
}
@Override
public String toString() {
- Map<String, Object> map = toYaml(new HashSet<>());
+ Map<String, Object> map = toYaml();
return YamlUtils.format(map);
}
}
diff --git
a/api/src/main/java/org/apache/unomi/api/conditions/ConditionType.java
b/api/src/main/java/org/apache/unomi/api/conditions/ConditionType.java
index 621e3aab8..cd4683f65 100644
--- a/api/src/main/java/org/apache/unomi/api/conditions/ConditionType.java
+++ b/api/src/main/java/org/apache/unomi/api/conditions/ConditionType.java
@@ -21,17 +21,10 @@ import org.apache.unomi.api.Metadata;
import org.apache.unomi.api.MetadataItem;
import org.apache.unomi.api.Parameter;
import org.apache.unomi.api.PluginType;
-import org.apache.unomi.api.utils.YamlUtils;
+import org.apache.unomi.api.utils.YamlUtils.*;
import javax.xml.bind.annotation.XmlElement;
-import java.util.ArrayList;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-import java.util.stream.Collectors;
-
-import static org.apache.unomi.api.utils.YamlUtils.*;
+import java.util.*;
/**
* ConditionTypes define new conditions that can be applied to items (for
example to decide whether a rule needs to be triggered or if a profile is
considered as taking part in a
@@ -39,7 +32,7 @@ import static org.apache.unomi.api.utils.YamlUtils.*;
* optimized by coding it. They may also be defined as combination of other
conditions. A simple condition could be: “User is male”, while a more generic
condition with
* parameters may test whether a given property has a specific value: “User
property x has value y”.
*/
-public class ConditionType extends MetadataItem implements PluginType {
+public class ConditionType extends MetadataItem implements PluginType,
YamlConvertible {
public static final String ITEM_TYPE = "conditionType";
private static final long serialVersionUID = -6965481691241954969L;
@@ -164,37 +157,36 @@ public class ConditionType extends MetadataItem
implements PluginType {
/**
* Converts this condition type to a Map structure for YAML output.
+ * Implements YamlConvertible interface with circular reference detection.
*
- * @param visited set of already visited condition types to prevent
infinite recursion
+ * @param visited set of already visited objects to prevent infinite
recursion (may be null)
* @return a Map representation of this condition type
*/
- public Map<String, Object> toYaml(Set<ConditionType> visited) {
- if (visited.contains(this)) {
+ @Override
+ public Map<String, Object> toYaml(Set<Object> visited, int maxDepth) {
+ if (maxDepth <= 0) {
+ return YamlMapBuilder.create()
+ .put("parentCondition", "<max depth exceeded>")
+ .put("parameters", "<max depth exceeded>")
+ .put("pluginId", pluginId)
+ .build();
+ }
+ if (visited != null && visited.contains(this)) {
return circularRef();
}
- visited.add(this);
+ final Set<Object> visitedSet = visited != null ? visited : new
HashSet<>();
+ visitedSet.add(this);
try {
return YamlMapBuilder.create()
- .putIfNotNull("id", itemId)
+ .mergeObject(super.toYaml(visitedSet, maxDepth))
.putIfNotNull("conditionEvaluator", conditionEvaluator)
.putIfNotNull("queryBuilder", queryBuilder)
- .putIfNotNull("parentCondition", parentCondition != null ?
parentCondition.toYaml(new HashSet<>()) : null)
- .putIfNotEmpty("parameters", parameters != null ?
parameters.stream()
- .map(Parameter::toYaml)
- .collect(Collectors.toList()) : null)
+ .putIfNotNull("parentCondition", parentCondition != null ?
toYamlValue(parentCondition, visitedSet, maxDepth - 1) : null)
+ .putIfNotEmpty("parameters", parameters != null ?
(Collection<?>) toYamlValue(parameters, visitedSet, maxDepth - 1) : null)
.put("pluginId", pluginId)
- .putIfNotNull("name", metadata != null ? metadata.getName() :
null)
- .putIfNotNull("description", metadata != null ?
metadata.getDescription() : null)
- .putIfNotNull("scope", metadata != null ? metadata.getScope()
: null)
.build();
} finally {
- visited.remove(this);
+ visitedSet.remove(this);
}
}
-
- @Override
- public String toString() {
- Map<String, Object> map = toYaml(new HashSet<>());
- return YamlUtils.format(map);
- }
}
diff --git
a/api/src/main/java/org/apache/unomi/api/conditions/ConditionValidation.java
b/api/src/main/java/org/apache/unomi/api/conditions/ConditionValidation.java
index 10aa0cd21..6bcd9aa8a 100644
--- a/api/src/main/java/org/apache/unomi/api/conditions/ConditionValidation.java
+++ b/api/src/main/java/org/apache/unomi/api/conditions/ConditionValidation.java
@@ -17,17 +17,18 @@
package org.apache.unomi.api.conditions;
import org.apache.unomi.api.utils.YamlUtils;
+import org.apache.unomi.api.utils.YamlUtils.YamlConvertible;
import java.io.Serializable;
import java.util.Map;
import java.util.Set;
-import static org.apache.unomi.api.utils.YamlUtils.*;
+import static org.apache.unomi.api.utils.YamlUtils.setToSortedList;
/**
* Validation metadata for condition parameters
*/
-public class ConditionValidation implements Serializable {
+public class ConditionValidation implements Serializable, YamlConvertible {
private static final long serialVersionUID = 1L;
public enum Type {
@@ -120,10 +121,13 @@ public class ConditionValidation implements Serializable {
/**
* Converts this validation to a Map structure for YAML output.
+ * Implements YamlConvertible interface.
*
+ * @param visited set of already visited objects to prevent infinite
recursion (may be null)
* @return a Map representation of this validation
*/
- public Map<String, Object> toYaml() {
+ @Override
+ public Map<String, Object> toYaml(Set<Object> visited, int maxDepth) {
return YamlUtils.YamlMapBuilder.create()
.putIf("required", true, required)
.putIf("recommended", true, recommended)
diff --git a/api/src/main/java/org/apache/unomi/api/goals/Goal.java
b/api/src/main/java/org/apache/unomi/api/goals/Goal.java
index 1b2ff0e88..12694b3d7 100644
--- a/api/src/main/java/org/apache/unomi/api/goals/Goal.java
+++ b/api/src/main/java/org/apache/unomi/api/goals/Goal.java
@@ -21,6 +21,15 @@ import org.apache.unomi.api.Metadata;
import org.apache.unomi.api.MetadataItem;
import org.apache.unomi.api.campaigns.Campaign;
import org.apache.unomi.api.conditions.Condition;
+import org.apache.unomi.api.utils.YamlUtils.YamlConvertible;
+import org.apache.unomi.api.utils.YamlUtils.YamlMapBuilder;
+
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+import static org.apache.unomi.api.utils.YamlUtils.circularRef;
+import static org.apache.unomi.api.utils.YamlUtils.toYamlValue;
/**
* A tracked activity / action that can be accomplished by site (scope)
visitors. These are tracked in general because they relate to specific business
objectives or are
@@ -32,7 +41,7 @@ import org.apache.unomi.api.conditions.Condition;
* <li>audience filtering: any visitor is considered for scope-level goals
while campaign-level goals only consider visitors who match the campaign's
conditions
* </ul>
*/
-public class Goal extends MetadataItem {
+public class Goal extends MetadataItem implements YamlConvertible {
public static final String ITEM_TYPE = "goal";
private static final long serialVersionUID = 6131648013470949983L;
private Condition startEvent;
@@ -87,4 +96,37 @@ public class Goal extends MetadataItem {
public void setCampaignId(String campaignId) {
this.campaignId = campaignId;
}
+
+ /**
+ * Converts this goal to a Map structure for YAML output.
+ * Implements YamlConvertible interface with circular reference detection.
+ *
+ * @param visited set of already visited objects to prevent infinite
recursion (may be null)
+ * @return a Map representation of this goal
+ */
+ @Override
+ public Map<String, Object> toYaml(Set<Object> visited, int maxDepth) {
+ if (maxDepth <= 0) {
+ return YamlMapBuilder.create()
+ .put("startEvent", "<max depth exceeded>")
+ .put("targetEvent", "<max depth exceeded>")
+ .put("campaignId", campaignId)
+ .build();
+ }
+ if (visited != null && visited.contains(this)) {
+ return circularRef();
+ }
+ final Set<Object> visitedSet = visited != null ? visited : new
HashSet<>();
+ visitedSet.add(this);
+ try {
+ return YamlMapBuilder.create()
+ .mergeObject(super.toYaml(visitedSet, maxDepth))
+ .putIfNotNull("startEvent", startEvent != null ?
toYamlValue(startEvent, visitedSet, maxDepth - 1) : null)
+ .putIfNotNull("targetEvent", targetEvent != null ?
toYamlValue(targetEvent, visitedSet, maxDepth - 1) : null)
+ .putIfNotNull("campaignId", campaignId)
+ .build();
+ } finally {
+ visitedSet.remove(this);
+ }
+ }
}
diff --git a/api/src/main/java/org/apache/unomi/api/rules/Rule.java
b/api/src/main/java/org/apache/unomi/api/rules/Rule.java
index 0aedfb123..63750c0d4 100644
--- a/api/src/main/java/org/apache/unomi/api/rules/Rule.java
+++ b/api/src/main/java/org/apache/unomi/api/rules/Rule.java
@@ -20,10 +20,15 @@ package org.apache.unomi.api.rules;
import org.apache.unomi.api.*;
import org.apache.unomi.api.actions.Action;
import org.apache.unomi.api.conditions.Condition;
+import org.apache.unomi.api.utils.YamlUtils.YamlConvertible;
+import org.apache.unomi.api.utils.YamlUtils.YamlMapBuilder;
-import java.util.List;
+import java.util.*;
import java.util.stream.Collectors;
+import static org.apache.unomi.api.utils.YamlUtils.circularRef;
+import static org.apache.unomi.api.utils.YamlUtils.toYamlValue;
+
/**
* A conditional set of actions to be executed in response to incoming events.
Triggering of rules is guarded by a condition: the rule is only triggered if
the associated
* condition ({@link #getCondition()}) is satisfied. Once a rule triggers, a
list of actions ({@link #getActions()} can be performed as consequences.
@@ -34,7 +39,7 @@ import java.util.stream.Collectors;
* We could also specify a priority for our rule in case it needs to be
executed before other ones when similar conditions match. This is accomplished
using the
* {@link #getPriority()} property.
*/
-public class Rule extends MetadataItem {
+public class Rule extends MetadataItem implements YamlConvertible {
/**
* The Rule ITEM_TYPE.
@@ -197,4 +202,41 @@ public class Rule extends MetadataItem {
public void setPriority(int priority) {
this.priority = priority;
}
+
+ /**
+ * Converts this rule to a Map structure for YAML output.
+ * Implements YamlConvertible interface with circular reference detection.
+ *
+ * @param visited set of already visited objects to prevent infinite
recursion (may be null)
+ * @return a Map representation of this rule
+ */
+ @Override
+ public Map<String, Object> toYaml(Set<Object> visited, int maxDepth) {
+ if (maxDepth <= 0) {
+ return YamlMapBuilder.create()
+ .put("condition", "<max depth exceeded>")
+ .put("actions", "<max depth exceeded>")
+ .put("priority", priority)
+ .build();
+ }
+ if (visited != null && visited.contains(this)) {
+ return circularRef();
+ }
+ final Set<Object> visitedSet = visited != null ? visited : new
HashSet<>();
+ visitedSet.add(this);
+ try {
+ return YamlMapBuilder.create()
+ .mergeObject(super.toYaml(visitedSet, maxDepth))
+ .putIfNotNull("condition", condition != null ?
toYamlValue(condition, visitedSet, maxDepth - 1) : null)
+ .putIfNotEmpty("actions", actions != null ? (Collection<?>)
toYamlValue(actions, visitedSet, maxDepth - 1) : null)
+ .putIfNotEmpty("linkedItems", linkedItems)
+ .putIf("raiseEventOnlyOnceForProfile", true,
raiseEventOnlyOnceForProfile)
+ .putIf("raiseEventOnlyOnceForSession", true,
raiseEventOnlyOnceForSession)
+ .putIf("raiseEventOnlyOnce", true, raiseEventOnlyOnce)
+ .putIf("priority", priority, priority != 0)
+ .build();
+ } finally {
+ visitedSet.remove(this);
+ }
+ }
}
diff --git a/api/src/main/java/org/apache/unomi/api/segments/Scoring.java
b/api/src/main/java/org/apache/unomi/api/segments/Scoring.java
index 6018fb376..3d82e6e7e 100644
--- a/api/src/main/java/org/apache/unomi/api/segments/Scoring.java
+++ b/api/src/main/java/org/apache/unomi/api/segments/Scoring.java
@@ -21,14 +21,19 @@ import org.apache.unomi.api.Item;
import org.apache.unomi.api.Metadata;
import org.apache.unomi.api.MetadataItem;
import org.apache.unomi.api.Profile;
+import org.apache.unomi.api.utils.YamlUtils.YamlConvertible;
+import org.apache.unomi.api.utils.YamlUtils.YamlMapBuilder;
-import java.util.List;
+import java.util.*;
+
+import static org.apache.unomi.api.utils.YamlUtils.circularRef;
+import static org.apache.unomi.api.utils.YamlUtils.toYamlValue;
/**
* A set of conditions associated with a value to assign to {@link Profile}s
when matching so that the associated users can be scored along that
* dimension. Each {@link ScoringElement} is evaluated and matching profiles'
scores are incremented with the associated value.
*/
-public class Scoring extends MetadataItem {
+public class Scoring extends MetadataItem implements YamlConvertible {
/**
* The Scoring ITEM_TYPE.
*
@@ -71,4 +76,33 @@ public class Scoring extends MetadataItem {
this.elements = elements;
}
+ /**
+ * Converts this scoring to a Map structure for YAML output.
+ * Implements YamlConvertible interface with circular reference detection.
+ *
+ * @param visited set of already visited objects to prevent infinite
recursion (may be null)
+ * @return a Map representation of this scoring
+ */
+ @Override
+ public Map<String, Object> toYaml(Set<Object> visited, int maxDepth) {
+ if (maxDepth <= 0) {
+ return YamlMapBuilder.create()
+ .put("elements", "<max depth exceeded>")
+ .build();
+ }
+ if (visited != null && visited.contains(this)) {
+ return circularRef();
+ }
+ final Set<Object> visitedSet = visited != null ? visited : new
HashSet<>();
+ visitedSet.add(this);
+ try {
+ return YamlMapBuilder.create()
+ .mergeObject(super.toYaml(visitedSet, maxDepth))
+ .putIfNotEmpty("elements", elements != null ? (Collection<?>)
toYamlValue(elements, visitedSet, maxDepth - 1) : null)
+ .build();
+ } finally {
+ visitedSet.remove(this);
+ }
+ }
+
}
diff --git
a/api/src/main/java/org/apache/unomi/api/segments/ScoringElement.java
b/api/src/main/java/org/apache/unomi/api/segments/ScoringElement.java
index 1af5147fb..bfc5a21cc 100644
--- a/api/src/main/java/org/apache/unomi/api/segments/ScoringElement.java
+++ b/api/src/main/java/org/apache/unomi/api/segments/ScoringElement.java
@@ -18,13 +18,22 @@
package org.apache.unomi.api.segments;
import org.apache.unomi.api.conditions.Condition;
+import org.apache.unomi.api.utils.YamlUtils;
+import org.apache.unomi.api.utils.YamlUtils.YamlConvertible;
+import org.apache.unomi.api.utils.YamlUtils.YamlMapBuilder;
import java.io.Serializable;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+import static org.apache.unomi.api.utils.YamlUtils.circularRef;
+import static org.apache.unomi.api.utils.YamlUtils.toYamlValue;
/**
* A scoring dimension along profiles can be evaluated and associated value to
be assigned.
*/
-public class ScoringElement implements Serializable {
+public class ScoringElement implements Serializable, YamlConvertible {
private Condition condition;
private int value;
@@ -69,4 +78,40 @@ public class ScoringElement implements Serializable {
public void setValue(int value) {
this.value = value;
}
+
+ /**
+ * Converts this scoring element to a Map structure for YAML output.
+ * Implements YamlConvertible interface with circular reference detection.
+ *
+ * @param visited set of already visited objects to prevent infinite
recursion (may be null)
+ * @return a Map representation of this scoring element
+ */
+ @Override
+ public Map<String, Object> toYaml(Set<Object> visited, int maxDepth) {
+ if (maxDepth <= 0) {
+ return YamlMapBuilder.create()
+ .put("condition", "<max depth exceeded>")
+ .put("value", value)
+ .build();
+ }
+ if (visited != null && visited.contains(this)) {
+ return circularRef();
+ }
+ final Set<Object> visitedSet = visited != null ? visited : new
HashSet<>();
+ visitedSet.add(this);
+ try {
+ return YamlMapBuilder.create()
+ .putIfNotNull("condition", condition != null ?
toYamlValue(condition, visitedSet, maxDepth - 1) : null)
+ .put("value", value)
+ .build();
+ } finally {
+ visitedSet.remove(this);
+ }
+ }
+
+ @Override
+ public String toString() {
+ Map<String, Object> map = toYaml();
+ return YamlUtils.format(map);
+ }
}
diff --git a/api/src/main/java/org/apache/unomi/api/segments/Segment.java
b/api/src/main/java/org/apache/unomi/api/segments/Segment.java
index 4e0d33830..2729ec74d 100644
--- a/api/src/main/java/org/apache/unomi/api/segments/Segment.java
+++ b/api/src/main/java/org/apache/unomi/api/segments/Segment.java
@@ -22,13 +22,22 @@ import org.apache.unomi.api.Metadata;
import org.apache.unomi.api.MetadataItem;
import org.apache.unomi.api.Profile;
import org.apache.unomi.api.conditions.Condition;
+import org.apache.unomi.api.utils.YamlUtils.YamlConvertible;
+import org.apache.unomi.api.utils.YamlUtils.YamlMapBuilder;
+
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+import static org.apache.unomi.api.utils.YamlUtils.circularRef;
+import static org.apache.unomi.api.utils.YamlUtils.toYamlValue;
/**
* A dynamically evaluated group of similar profiles in order to categorize
the associated users. To be considered part of a given segment, users must
satisfies
* the segment’s condition. If they match, users are automatically added to
the segment. Similarly, if at any given point during, they cease to satisfy the
segment’s condition,
* they are automatically removed from it.
*/
-public class Segment extends MetadataItem {
+public class Segment extends MetadataItem implements YamlConvertible {
/**
* The Segment ITEM_TYPE.
@@ -72,4 +81,33 @@ public class Segment extends MetadataItem {
this.condition = condition;
}
+ /**
+ * Converts this segment to a Map structure for YAML output.
+ * Implements YamlConvertible interface with circular reference detection.
+ *
+ * @param visited set of already visited objects to prevent infinite
recursion (may be null)
+ * @return a Map representation of this segment
+ */
+ @Override
+ public Map<String, Object> toYaml(Set<Object> visited, int maxDepth) {
+ if (maxDepth <= 0) {
+ return YamlMapBuilder.create()
+ .put("condition", "<max depth exceeded>")
+ .build();
+ }
+ if (visited != null && visited.contains(this)) {
+ return circularRef();
+ }
+ final Set<Object> visitedSet = visited != null ? visited : new
HashSet<>();
+ visitedSet.add(this);
+ try {
+ return YamlMapBuilder.create()
+ .mergeObject(super.toYaml(visitedSet, maxDepth))
+ .putIfNotNull("condition", condition != null ?
toYamlValue(condition, visitedSet, maxDepth - 1) : null)
+ .build();
+ } finally {
+ visitedSet.remove(this);
+ }
+ }
+
}
diff --git a/api/src/main/java/org/apache/unomi/api/utils/YamlUtils.java
b/api/src/main/java/org/apache/unomi/api/utils/YamlUtils.java
index 4dd4cce85..664f63fb7 100644
--- a/api/src/main/java/org/apache/unomi/api/utils/YamlUtils.java
+++ b/api/src/main/java/org/apache/unomi/api/utils/YamlUtils.java
@@ -48,12 +48,38 @@ public class YamlUtils {
* Interface for objects that can convert themselves to YAML Map
structures.
*/
public interface YamlConvertible {
+ /**
+ * Converts this object to a Map structure for YAML output with depth
limiting.
+ * This method accepts an optional visited set to detect circular
references and a max depth
+ * to prevent StackOverflowError from extremely deep nested structures.
+ *
+ * @param visited optional set of visited objects to detect circular
references (may be null)
+ * @param maxDepth maximum recursion depth (prevents
StackOverflowError from deep nesting)
+ * @return a Map representation of this object
+ */
+ Map<String, Object> toYaml(Set<Object> visited, int maxDepth);
+
/**
* Converts this object to a Map structure for YAML output.
+ * This method accepts an optional visited set to detect circular
references.
+ * Uses a default max depth of 20 to prevent StackOverflowError.
*
+ * @param visited optional set of visited objects to detect circular
references (may be null)
* @return a Map representation of this object
*/
- Map<String, Object> toYaml();
+ default Map<String, Object> toYaml(Set<Object> visited) {
+ return toYaml(visited, 20);
+ }
+
+ /**
+ * Converts this object to a Map structure for YAML output.
+ * This is a convenience method that calls toYaml(null, 20).
+ *
+ * @return a Map representation of this object
+ */
+ default Map<String, Object> toYaml() {
+ return toYaml(null, 20);
+ }
}
/**
@@ -147,6 +173,28 @@ public class YamlUtils {
return this;
}
+ /**
+ * Merges all fields from a Map into this builder.
+ * This is useful for inheritance where subclasses want to include
parent class fields.
+ *
+ * Usage in subclasses:
+ * <pre>
+ * return YamlMapBuilder.create()
+ * .mergeObject(super.toYaml(visitedSet))
+ * .putIfNotNull("field", value)
+ * .build();
+ * </pre>
+ *
+ * @param objectMap the Map containing fields to merge (may be null,
in which case nothing is merged)
+ * @return this builder for chaining
+ */
+ public YamlMapBuilder mergeObject(Map<String, Object> objectMap) {
+ if (objectMap != null) {
+ objectMap.forEach(map::put);
+ }
+ return this;
+ }
+
/**
* Builds and returns a defensive copy of the map.
*
@@ -190,35 +238,63 @@ public class YamlUtils {
/**
* Converts a value to YAML-compatible format, handling nested structures.
- * Note: This method does not perform circular reference detection for
generic objects.
- * For objects that implement YamlConvertible, circular reference
detection should be
- * handled in their toYaml() implementation.
+ * For objects that implement YamlConvertible, circular reference
detection is
+ * handled by passing the visited set to their toYaml() implementation.
*
* @param value the value to convert
- * @param visited set of visited objects (currently unused, reserved for
future circular reference detection)
+ * @param visited set of visited objects for circular reference detection
(may be null)
* @return the converted value
*/
public static Object toYamlValue(Object value, Set<Object> visited) {
+ return toYamlValue(value, visited, Integer.MAX_VALUE);
+ }
+
+ /**
+ * Converts a value to YAML-compatible format with depth limiting to
prevent StackOverflowError.
+ * For objects that implement YamlConvertible, circular reference
detection is
+ * handled by passing the visited set to their toYaml() implementation.
+ *
+ * @param value the value to convert
+ * @param visited set of visited objects for circular reference detection
(may be null)
+ * @param maxDepth maximum recursion depth (prevents StackOverflowError
from deep nesting)
+ * @return the converted value, or a placeholder if max depth exceeded
+ */
+ public static Object toYamlValue(Object value, Set<Object> visited, int
maxDepth) {
+ if (maxDepth <= 0) {
+ return "<max depth exceeded>";
+ }
if (value == null) {
return null;
}
if (value instanceof YamlConvertible) {
- return ((YamlConvertible) value).toYaml();
+ // For YamlConvertible, get the Map and then process it as a Map
to ensure sorting
+ // Pass maxDepth - 1 to the toYaml method to continue depth
limiting
+ Map<String, Object> result = ((YamlConvertible)
value).toYaml(visited, maxDepth - 1);
+ // Process the result as a Map to ensure it's sorted (this handles
both sorting and recursive processing)
+ return toYamlValue(result, visited, maxDepth - 1);
}
if (value instanceof List) {
return ((List<?>) value).stream()
- .map(item -> toYamlValue(item, visited))
+ .map(item -> toYamlValue(item, visited, maxDepth - 1))
.collect(Collectors.toList());
}
if (value instanceof Map) {
+ Map<?, ?> inputMap = (Map<?, ?>) value;
Map<String, Object> result = new LinkedHashMap<>();
- ((Map<?, ?>) value).forEach((key, val) ->
- result.put(String.valueOf(key), toYamlValue(val, visited)));
+
+ if (!inputMap.isEmpty()) {
+ // Sort entries alphabetically by key string representation
+ inputMap.entrySet().stream()
+ .sorted((e1, e2) ->
String.valueOf(e1.getKey()).compareTo(String.valueOf(e2.getKey())))
+ .forEach(entry ->
+ result.put(String.valueOf(entry.getKey()),
toYamlValue(entry.getValue(), visited, maxDepth - 1)));
+ }
return result;
}
return value;
}
+
/**
* Formats a value as YAML using SnakeYaml.
* This is a convenience method that delegates to SnakeYaml.
diff --git a/api/src/test/java/org/apache/unomi/api/utils/YamlUtilsTest.java
b/api/src/test/java/org/apache/unomi/api/utils/YamlUtilsTest.java
index 95a00e523..c40c066ca 100644
--- a/api/src/test/java/org/apache/unomi/api/utils/YamlUtilsTest.java
+++ b/api/src/test/java/org/apache/unomi/api/utils/YamlUtilsTest.java
@@ -16,6 +16,10 @@
*/
package org.apache.unomi.api.utils;
+import org.apache.unomi.api.Metadata;
+import org.apache.unomi.api.actions.Action;
+import org.apache.unomi.api.conditions.Condition;
+import org.apache.unomi.api.rules.Rule;
import org.junit.Test;
import java.util.*;
@@ -189,7 +193,7 @@ public class YamlUtilsTest {
@Test
public void testToYamlValueWithYamlConvertible() {
- YamlUtils.YamlConvertible convertible = () -> {
+ YamlUtils.YamlConvertible convertible = (visited, maxDepth) -> {
Map<String, Object> map = new LinkedHashMap<>();
map.put("test", "value");
return map;
@@ -252,4 +256,355 @@ public class YamlUtilsTest {
assertTrue("Format should contain key", result.contains("key"));
assertTrue("Format should contain value", result.contains("value"));
}
+
+ // ========== Circular Reference Detection Tests ==========
+
+ @Test
+ public void testRuleInheritanceChainNoCircularRef() {
+ // Test that Rule -> MetadataItem -> Item inheritance chain doesn't
produce false circular refs
+ Rule rule = new Rule();
+ rule.setItemId("test-rule");
+ Metadata metadata = new Metadata("test-rule");
+ metadata.setScope("systemscope");
+ rule.setMetadata(metadata);
+
+ Condition condition = new Condition();
+ condition.setConditionTypeId("testCondition");
+ rule.setCondition(condition);
+
+ Map<String, Object> result = rule.toYaml(null);
+ assertNotNull("Rule should serialize to YAML", result);
+ assertFalse("Should not contain circular reference marker",
result.containsKey("$ref"));
+ assertTrue("Should contain condition",
result.containsKey("condition"));
+ assertTrue("Should contain itemId from Item parent",
result.containsKey("itemId"));
+ assertTrue("Should contain metadata from MetadataItem parent",
result.containsKey("metadata"));
+ }
+
+ @Test
+ public void testRuleWithCircularReferenceInCondition() {
+ // Test that a real circular reference (Rule referenced in condition's
parameterValues) is detected
+ Rule rule = new Rule();
+ rule.setItemId("test-rule");
+ Metadata metadata = new Metadata("test-rule");
+ rule.setMetadata(metadata);
+
+ Condition condition = new Condition();
+ condition.setConditionTypeId("testCondition");
+ // Create a circular reference: condition's parameterValues contains
the rule itself
+ condition.getParameterValues().put("referencedRule", rule);
+ rule.setCondition(condition);
+
+ Map<String, Object> result = rule.toYaml(null);
+ assertNotNull("Rule should serialize to YAML", result);
+ assertTrue("Should contain condition",
result.containsKey("condition"));
+
+ // Check that the circular reference is detected in the condition's
parameterValues
+ Map<String, Object> conditionMap = (Map<String, Object>)
result.get("condition");
+ assertNotNull("Condition should be serialized", conditionMap);
+ Map<String, Object> paramValues = (Map<String, Object>)
conditionMap.get("parameterValues");
+ assertNotNull("Parameter values should exist", paramValues);
+ Map<String, Object> circularRef = (Map<String, Object>)
paramValues.get("referencedRule");
+ assertNotNull("Circular reference should be detected", circularRef);
+ assertEquals("Should contain circular reference marker", "circular",
circularRef.get("$ref"));
+ }
+
+ @Test
+ public void testRuleWithCircularReferenceInActions() {
+ // Test circular reference in actions list
+ Rule rule = new Rule();
+ rule.setItemId("test-rule");
+ Metadata metadata = new Metadata("test-rule");
+ rule.setMetadata(metadata);
+
+ Action action = new Action();
+ action.setActionTypeId("testAction");
+ // Create circular reference: action's parameterValues contains the
rule
+ action.getParameterValues().put("triggeringRule", rule);
+ rule.setActions(Collections.singletonList(action));
+
+ Map<String, Object> result = rule.toYaml(null);
+ assertNotNull("Rule should serialize to YAML", result);
+ assertTrue("Should contain actions", result.containsKey("actions"));
+
+ List<?> actions = (List<?>) result.get("actions");
+ assertNotNull("Actions list should exist", actions);
+ assertEquals("Should have one action", 1, actions.size());
+
+ Map<String, Object> actionMap = (Map<String, Object>) actions.get(0);
+ Map<String, Object> paramValues = (Map<String, Object>)
actionMap.get("parameterValues");
+ assertNotNull("Parameter values should exist", paramValues);
+ Map<String, Object> circularRef = (Map<String, Object>)
paramValues.get("triggeringRule");
+ assertNotNull("Circular reference should be detected", circularRef);
+ assertEquals("Should contain circular reference marker", "circular",
circularRef.get("$ref"));
+ }
+
+ @Test
+ public void testNestedCircularReference() {
+ // Test nested circular reference: Rule -> Condition -> nested
Condition -> Rule
+ Rule rule = new Rule();
+ rule.setItemId("test-rule");
+ Metadata metadata = new Metadata("test-rule");
+ rule.setMetadata(metadata);
+
+ Condition outerCondition = new Condition();
+ outerCondition.setConditionTypeId("outerCondition");
+
+ Condition nestedCondition = new Condition();
+ nestedCondition.setConditionTypeId("nestedCondition");
+ // Nested condition references the rule
+ nestedCondition.getParameterValues().put("ruleRef", rule);
+
+ // Outer condition contains nested condition
+ outerCondition.getParameterValues().put("nested", nestedCondition);
+ rule.setCondition(outerCondition);
+
+ Map<String, Object> result = rule.toYaml(null);
+ assertNotNull("Rule should serialize to YAML", result);
+
+ // Navigate through the nested structure
+ Map<String, Object> conditionMap = (Map<String, Object>)
result.get("condition");
+ Map<String, Object> paramValues = (Map<String, Object>)
conditionMap.get("parameterValues");
+ Map<String, Object> nestedConditionMap = (Map<String, Object>)
paramValues.get("nested");
+ Map<String, Object> nestedParamValues = (Map<String, Object>)
nestedConditionMap.get("parameterValues");
+ Map<String, Object> circularRef = (Map<String, Object>)
nestedParamValues.get("ruleRef");
+
+ assertNotNull("Circular reference should be detected in nested
structure", circularRef);
+ assertEquals("Should contain circular reference marker", "circular",
circularRef.get("$ref"));
+ }
+
+ @Test
+ public void testMultipleCircularReferences() {
+ // Test multiple circular references to the same object
+ Rule rule = new Rule();
+ rule.setItemId("test-rule");
+ Metadata metadata = new Metadata("test-rule");
+ rule.setMetadata(metadata);
+
+ Condition condition = new Condition();
+ condition.setConditionTypeId("testCondition");
+ // Multiple references to the same rule
+ condition.getParameterValues().put("rule1", rule);
+ condition.getParameterValues().put("rule2", rule);
+ condition.getParameterValues().put("rule3", rule);
+ rule.setCondition(condition);
+
+ Map<String, Object> result = rule.toYaml(null);
+ Map<String, Object> conditionMap = (Map<String, Object>)
result.get("condition");
+ Map<String, Object> paramValues = (Map<String, Object>)
conditionMap.get("parameterValues");
+
+ // All three references should show circular ref
+ for (String key : Arrays.asList("rule1", "rule2", "rule3")) {
+ Map<String, Object> circularRef = (Map<String, Object>)
paramValues.get(key);
+ assertNotNull("Circular reference should be detected for " + key,
circularRef);
+ assertEquals("Should contain circular reference marker for " +
key, "circular", circularRef.get("$ref"));
+ }
+ }
+
+
+ @Test
+ public void testCircularReferenceInList() {
+ // Test circular reference in a list
+ Rule rule = new Rule();
+ rule.setItemId("test-rule");
+ Metadata metadata = new Metadata("test-rule");
+ rule.setMetadata(metadata);
+
+ Condition condition = new Condition();
+ condition.setConditionTypeId("testCondition");
+ // List containing the rule itself
+ condition.getParameterValues().put("ruleList", Arrays.asList(rule,
"other", rule));
+ rule.setCondition(condition);
+
+ Map<String, Object> result = rule.toYaml(null);
+ Map<String, Object> conditionMap = (Map<String, Object>)
result.get("condition");
+ Map<String, Object> paramValues = (Map<String, Object>)
conditionMap.get("parameterValues");
+ List<?> ruleList = (List<?>) paramValues.get("ruleList");
+
+ assertNotNull("Rule list should exist", ruleList);
+ assertEquals("List should have 3 elements", 3, ruleList.size());
+
+ // First element should be circular ref
+ Map<String, Object> circularRef1 = (Map<String, Object>)
ruleList.get(0);
+ assertEquals("First element should be circular ref", "circular",
circularRef1.get("$ref"));
+
+ // Second element should be string
+ assertEquals("Second element should be string", "other",
ruleList.get(1));
+
+ // Third element should also be circular ref
+ Map<String, Object> circularRef2 = (Map<String, Object>)
ruleList.get(2);
+ assertEquals("Third element should be circular ref", "circular",
circularRef2.get("$ref"));
+ }
+
+ @Test
+ public void testCircularReferenceInNestedMap() {
+ // Test circular reference in nested map structure
+ Rule rule = new Rule();
+ rule.setItemId("test-rule");
+ Metadata metadata = new Metadata("test-rule");
+ rule.setMetadata(metadata);
+
+ Condition condition = new Condition();
+ condition.setConditionTypeId("testCondition");
+ // Nested map containing the rule
+ Map<String, Object> nestedMap = new HashMap<>();
+ nestedMap.put("level1", new HashMap<String, Object>() {{
+ put("level2", new HashMap<String, Object>() {{
+ put("rule", rule);
+ }});
+ }});
+ condition.getParameterValues().put("nested", nestedMap);
+ rule.setCondition(condition);
+
+ Map<String, Object> result = rule.toYaml(null);
+ Map<String, Object> conditionMap = (Map<String, Object>)
result.get("condition");
+ Map<String, Object> paramValues = (Map<String, Object>)
conditionMap.get("parameterValues");
+ Map<String, Object> nested = (Map<String, Object>)
paramValues.get("nested");
+ Map<String, Object> level1 = (Map<String, Object>)
nested.get("level1");
+ Map<String, Object> level2 = (Map<String, Object>)
level1.get("level2");
+ Map<String, Object> circularRef = (Map<String, Object>)
level2.get("rule");
+
+ assertNotNull("Circular reference should be detected in nested map",
circularRef);
+ assertEquals("Should contain circular reference marker", "circular",
circularRef.get("$ref"));
+ }
+
+ @Test
+ public void testNoFalseCircularRefInInheritance() {
+ // Test that inheritance chain (Rule -> MetadataItem -> Item) doesn't
create false circular refs
+ // This is the main bug we're fixing
+ Rule rule = new Rule();
+ rule.setItemId("test-rule");
+ Metadata metadata = new Metadata("test-rule");
+ metadata.setScope("systemscope");
+ rule.setMetadata(metadata);
+
+ Condition condition = new Condition();
+ condition.setConditionTypeId("unavailableConditionType");
+ condition.getParameterValues().put("comparisonOperator", "equals");
+ condition.getParameterValues().put("propertyName", "testProperty");
+ condition.getParameterValues().put("propertyValue", "testValue");
+ rule.setCondition(condition);
+
+ Action action = new Action();
+ action.setActionTypeId("test");
+ rule.setActions(Collections.singletonList(action));
+
+ Map<String, Object> result = rule.toYaml(null);
+
+ // Should NOT contain $ref: circular at the top level
+ assertNotNull("Rule should serialize", result);
+ assertFalse("Should not have false circular reference at top level",
+ result.containsKey("$ref") &&
"circular".equals(result.get("$ref")));
+
+ // Should contain all expected fields from inheritance chain
+ assertTrue("Should contain itemId from Item",
result.containsKey("itemId"));
+ assertTrue("Should contain itemType from Item",
result.containsKey("itemType"));
+ assertEquals("itemType should be 'rule'", "rule",
result.get("itemType"));
+ assertTrue("Should contain metadata from MetadataItem",
result.containsKey("metadata"));
+ assertTrue("Should contain condition",
result.containsKey("condition"));
+ assertTrue("Should contain actions", result.containsKey("actions"));
+
+ // Verify condition structure
+ Map<String, Object> conditionMap = (Map<String, Object>)
result.get("condition");
+ assertNotNull("Condition should be present", conditionMap);
+ assertEquals("Condition should have correct type",
"unavailableConditionType", conditionMap.get("type"));
+
+ // Verify actions structure
+ List<?> actions = (List<?>) result.get("actions");
+ assertNotNull("Actions should be present", actions);
+ assertEquals("Should have one action", 1, actions.size());
+ }
+
+ @Test
+ public void testItemTypeIsAlwaysIncluded() {
+ // Test that itemType is always included in YAML output, even if null
+ // This reflects the actual state of the object
+ Rule rule = new Rule();
+ Metadata metadata = new Metadata("test-id");
+ metadata.setScope("systemscope");
+ rule.setMetadata(metadata);
+
+ Map<String, Object> result = rule.toYaml(null);
+
+ // itemType should always be present in output (set in Item
constructor for Rule)
+ assertTrue("itemType should be included",
result.containsKey("itemType"));
+ assertEquals("itemType should be 'rule'", "rule",
result.get("itemType"));
+
+ // itemId should also always be included
+ assertTrue("itemId should be included", result.containsKey("itemId"));
+ }
+
+ @Test
+ public void testItemIdAndItemTypeIncludedEvenWhenNull() {
+ // Test that itemId and itemType are always included, even when null
+ // This ensures YAML output reflects the actual state of the object
+ Rule rule = new Rule();
+ // Explicitly set itemId and itemType to null to test null handling
+ rule.setItemId(null);
+ rule.setItemType(null);
+
+ Map<String, Object> result = rule.toYaml(null);
+
+ // Both should be included even if null
+ assertTrue("itemId should be included even when null",
result.containsKey("itemId"));
+ assertNull("itemId should be null", result.get("itemId"));
+
+ assertTrue("itemType should be included even when null",
result.containsKey("itemType"));
+ assertNull("itemType should be null", result.get("itemType"));
+ }
+
+ @Test
+ public void testItemIdFromMetadata() {
+ // Test that itemId is set from metadata and included in YAML
+ Rule rule = new Rule();
+ Metadata metadata = new Metadata("test-rule-id");
+ metadata.setScope("systemscope");
+ rule.setMetadata(metadata);
+
+ Map<String, Object> result = rule.toYaml(null);
+
+ // itemId should be set from metadata.getId()
+ assertTrue("itemId should be included when set from metadata",
result.containsKey("itemId"));
+ assertEquals("itemId should match metadata id", "test-rule-id",
result.get("itemId"));
+ }
+
+ @Test
+ public void testVisitedSetIsSharedCorrectly() {
+ // Test that visited set is properly shared across nested calls
+ Rule rule1 = new Rule();
+ rule1.setItemId("rule1");
+ rule1.setMetadata(new Metadata("rule1"));
+
+ Rule rule2 = new Rule();
+ rule2.setItemId("rule2");
+ rule2.setMetadata(new Metadata("rule2"));
+
+ // rule1 references rule2, rule2 references rule1 (mutual circular
reference)
+ Condition condition1 = new Condition();
+ condition1.setConditionTypeId("test");
+ condition1.getParameterValues().put("otherRule", rule2);
+ rule1.setCondition(condition1);
+
+ Condition condition2 = new Condition();
+ condition2.setConditionTypeId("test");
+ condition2.getParameterValues().put("otherRule", rule1);
+ rule2.setCondition(condition2);
+
+ // Serialize rule1 - should detect circular ref when it encounters
rule2 which references rule1
+ Map<String, Object> result1 = rule1.toYaml(null);
+ assertNotNull("Rule1 should serialize", result1);
+
+ Map<String, Object> conditionMap1 = (Map<String, Object>)
result1.get("condition");
+ Map<String, Object> paramValues1 = (Map<String, Object>)
conditionMap1.get("parameterValues");
+ Map<String, Object> rule2Ref = (Map<String, Object>)
paramValues1.get("otherRule");
+
+ // rule2 should be serialized, but when it tries to reference rule1,
it should detect circular ref
+ assertNotNull("Rule2 reference should exist", rule2Ref);
+ // rule2 itself should be fully serialized (not circular), but its
condition's otherRule should be circular
+ Map<String, Object> conditionMap2 = (Map<String, Object>)
rule2Ref.get("condition");
+ assertNotNull("Rule2's condition should exist", conditionMap2);
+ Map<String, Object> paramValues2 = (Map<String, Object>)
conditionMap2.get("parameterValues");
+ Map<String, Object> rule1CircularRef = (Map<String, Object>)
paramValues2.get("otherRule");
+ assertNotNull("Circular reference to rule1 should be detected",
rule1CircularRef);
+ assertEquals("Should contain circular reference marker", "circular",
rule1CircularRef.get("$ref"));
+ }
}
diff --git
a/tracing/tracing-impl/src/main/java/org/apache/unomi/tracing/impl/DefaultRequestTracer.java
b/tracing/tracing-impl/src/main/java/org/apache/unomi/tracing/impl/DefaultRequestTracer.java
index e3c11962b..d15d87b06 100644
---
a/tracing/tracing-impl/src/main/java/org/apache/unomi/tracing/impl/DefaultRequestTracer.java
+++
b/tracing/tracing-impl/src/main/java/org/apache/unomi/tracing/impl/DefaultRequestTracer.java
@@ -19,7 +19,8 @@ package org.apache.unomi.tracing.impl;
import org.apache.unomi.tracing.api.RequestTracer;
import org.apache.unomi.tracing.api.TraceNode;
-import java.util.*;
+import java.util.Collection;
+import java.util.Stack;
/**
* Default implementation of the RequestTracer interface that stores trace
information in a tree structure
@@ -30,6 +31,24 @@ public class DefaultRequestTracer implements RequestTracer {
private final ThreadLocal<TraceNode> currentNode = new ThreadLocal<>();
private final ThreadLocal<TraceNode> rootNode = new ThreadLocal<>();
private final ThreadLocal<Stack<TraceNode>> nodeStack =
ThreadLocal.withInitial(Stack::new);
+ private static final int MAX_CONTEXT_STRING_LENGTH = 4096;
+
+ private static String safeContextToString(Object context) {
+ if (context == null) {
+ return "null";
+ }
+ try {
+ String rendered = String.valueOf(context);
+ if (rendered != null && rendered.length() >
MAX_CONTEXT_STRING_LENGTH) {
+ return rendered.substring(0, MAX_CONTEXT_STRING_LENGTH) +
"...(truncated)";
+ }
+ return rendered;
+ } catch (StackOverflowError e) {
+ return "<context-toString StackOverflowError class=" +
context.getClass().getName() + ">";
+ } catch (Throwable t) {
+ return "<context-toString failed class=" +
context.getClass().getName() + " error=" + t.getClass().getName() + ">";
+ }
+ }
@Override
public void startOperation(String operationType, String description,
Object context) {
@@ -42,7 +61,7 @@ public class DefaultRequestTracer implements RequestTracer {
node.setDescription(description);
node.setContext(context);
node.setStartTime(System.currentTimeMillis());
-
+
if (rootNode.get() == null) {
rootNode.set(node);
currentNode.set(node);
@@ -81,7 +100,7 @@ public class DefaultRequestTracer implements RequestTracer {
TraceNode node = currentNode.get();
if (node != null) {
if (context != null) {
- node.getTraces().add(message + " - Context: " + context);
+ node.getTraces().add(message + " - Context: " +
safeContextToString(context));
} else {
node.getTraces().add(message);
}
@@ -96,7 +115,7 @@ public class DefaultRequestTracer implements RequestTracer {
TraceNode node = currentNode.get();
if (node != null) {
- node.getTraces().add("Validation against schema " + schemaId + ":
" + validationMessages);
+ node.getTraces().add("Validation against schema " + schemaId + ":
" + safeContextToString(validationMessages));
}
}
@@ -124,4 +143,4 @@ public class DefaultRequestTracer implements RequestTracer {
currentNode.remove();
nodeStack.get().clear();
}
-}
\ No newline at end of file
+}
diff --git
a/tracing/tracing-impl/src/test/java/org/apache/unomi/tracing/impl/DefaultTracerServiceTest.java
b/tracing/tracing-impl/src/test/java/org/apache/unomi/tracing/impl/DefaultTracerServiceTest.java
index c36211911..d808fc09d 100644
---
a/tracing/tracing-impl/src/test/java/org/apache/unomi/tracing/impl/DefaultTracerServiceTest.java
+++
b/tracing/tracing-impl/src/test/java/org/apache/unomi/tracing/impl/DefaultTracerServiceTest.java
@@ -18,9 +18,9 @@ package org.apache.unomi.tracing.impl;
import org.apache.unomi.tracing.api.RequestTracer;
import org.apache.unomi.tracing.api.TraceNode;
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
import java.util.Arrays;
import java.util.concurrent.CountDownLatch;
@@ -28,7 +28,7 @@ import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
-import static org.junit.Assert.*;
+import static org.junit.jupiter.api.Assertions.*;
/**
* Unit tests for {@link DefaultTracerService}
@@ -39,12 +39,12 @@ public class DefaultTracerServiceTest {
private static final int THREAD_COUNT = 10;
private static final int TIMEOUT_SECONDS = 10;
- @Before
+ @BeforeEach
public void setUp() {
tracerService = new DefaultTracerService();
}
- @After
+ @AfterEach
public void tearDown() {
tracerService.cleanup();
}
@@ -52,32 +52,32 @@ public class DefaultTracerServiceTest {
@Test
public void testGetCurrentTracer() {
RequestTracer tracer = tracerService.getCurrentTracer();
- assertNotNull("Current tracer should not be null", tracer);
- assertTrue("Current tracer should be an instance of
DefaultRequestTracer", tracer instanceof DefaultRequestTracer);
+ assertNotNull(tracer, "Current tracer should not be null");
+ assertTrue(tracer instanceof DefaultRequestTracer, "Current tracer
should be an instance of DefaultRequestTracer");
// Test that we get the same tracer instance for the same thread
RequestTracer secondTracer = tracerService.getCurrentTracer();
- assertSame("Should get same tracer instance within same thread",
tracer, secondTracer);
+ assertSame(tracer, secondTracer, "Should get same tracer instance
within same thread");
}
@Test
public void testEnableDisableTracing() {
- assertFalse("Tracing should be disabled by default",
tracerService.isTracingEnabled());
+ assertFalse(tracerService.isTracingEnabled(), "Tracing should be
disabled by default");
tracerService.enableTracing();
- assertTrue("Tracing should be enabled after enableTracing()",
tracerService.isTracingEnabled());
+ assertTrue(tracerService.isTracingEnabled(), "Tracing should be
enabled after enableTracing()");
tracerService.disableTracing();
- assertFalse("Tracing should be disabled after disableTracing()",
tracerService.isTracingEnabled());
+ assertFalse(tracerService.isTracingEnabled(), "Tracing should be
disabled after disableTracing()");
// Verify that enable/disable resets the trace
tracerService.enableTracing();
RequestTracer tracer = tracerService.getCurrentTracer();
tracer.startOperation("test", "description", null);
- assertNotNull("Should have trace node after operation",
tracerService.getTraceNode());
+ assertNotNull(tracerService.getTraceNode(), "Should have trace node
after operation");
tracerService.disableTracing();
- assertNull("Trace node should be reset after disable",
tracerService.getTraceNode());
+ assertNull(tracerService.getTraceNode(), "Trace node should be reset
after disable");
}
@Test
@@ -104,23 +104,23 @@ public class DefaultTracerServiceTest {
// Get and verify the trace tree
TraceNode rootNode = tracerService.getTraceNode();
- assertNotNull("Root node should not be null", rootNode);
- assertEquals("Root operation type should match", "test",
rootNode.getOperationType());
- assertEquals("Root description should match", "Root completed",
rootNode.getDescription());
- assertEquals("Root result should match", "root-result",
rootNode.getResult());
- assertEquals("Root should have 2 traces", 2,
rootNode.getTraces().size());
- assertEquals("Root should have 1 child", 1,
rootNode.getChildren().size());
- assertTrue("Root start time should be valid", rootNode.getStartTime()
>= startTime);
- assertTrue("Root end time should be valid", rootNode.getEndTime() <=
endTime);
+ assertNotNull(rootNode, "Root node should not be null");
+ assertEquals("test", rootNode.getOperationType(), "Root operation type
should match");
+ assertEquals("Root completed", rootNode.getDescription(), "Root
description should match");
+ assertEquals("root-result", rootNode.getResult(), "Root result should
match");
+ assertEquals(2, rootNode.getTraces().size(), "Root should have 2
traces");
+ assertEquals(1, rootNode.getChildren().size(), "Root should have 1
child");
+ assertTrue(rootNode.getStartTime() >= startTime, "Root start time
should be valid");
+ assertTrue(rootNode.getEndTime() <= endTime, "Root end time should be
valid");
TraceNode childNode = rootNode.getChildren().get(0);
- assertEquals("Child operation type should match", "child",
childNode.getOperationType());
- assertEquals("Child description should match", "Child completed",
childNode.getDescription());
- assertEquals("Child context should match", "child-context",
childNode.getContext());
- assertEquals("Child result should match", "child-result",
childNode.getResult());
- assertEquals("Child should have 1 trace", 1,
childNode.getTraces().size());
- assertTrue("Child start time should be after root",
childNode.getStartTime() >= rootNode.getStartTime());
- assertTrue("Child end time should be before root end",
childNode.getEndTime() <= rootNode.getEndTime());
+ assertEquals("child", childNode.getOperationType(), "Child operation
type should match");
+ assertEquals("Child completed", childNode.getDescription(), "Child
description should match");
+ assertEquals("child-context", childNode.getContext(), "Child context
should match");
+ assertEquals("child-result", childNode.getResult(), "Child result
should match");
+ assertEquals(1, childNode.getTraces().size(), "Child should have 1
trace");
+ assertTrue(childNode.getStartTime() >= rootNode.getStartTime(), "Child
start time should be after root");
+ assertTrue(childNode.getEndTime() <= rootNode.getEndTime(), "Child end
time should be before root end");
}
@Test
@@ -133,12 +133,12 @@ public class DefaultTracerServiceTest {
tracer.endOperation(false, "Validation failed");
TraceNode node = tracerService.getTraceNode();
- assertNotNull("Node should not be null", node);
- assertEquals("Should have 1 validation trace", 1,
node.getTraces().size());
+ assertNotNull(node, "Node should not be null");
+ assertEquals(1, node.getTraces().size(), "Should have 1 validation
trace");
String validationTrace = node.getTraces().get(0);
- assertTrue("Validation trace should contain schema id",
validationTrace.contains("test-schema"));
- assertTrue("Validation trace should contain first error",
validationTrace.contains("error1"));
- assertTrue("Validation trace should contain second error",
validationTrace.contains("error2"));
+ assertTrue(validationTrace.contains("test-schema"), "Validation trace
should contain schema id");
+ assertTrue(validationTrace.contains("error1"), "Validation trace
should contain first error");
+ assertTrue(validationTrace.contains("error2"), "Validation trace
should contain second error");
}
@Test
@@ -151,7 +151,7 @@ public class DefaultTracerServiceTest {
tracer.addValidationInfo(Arrays.asList("error"), "schema");
tracer.endOperation("result", "Completed");
- assertNull("No trace node should be created when tracing is disabled",
tracerService.getTraceNode());
+ assertNull(tracerService.getTraceNode(), "No trace node should be
created when tracing is disabled");
}
@Test
@@ -161,13 +161,13 @@ public class DefaultTracerServiceTest {
tracer.startOperation("test", "Test operation", null);
tracer.trace("Test message", null);
- assertNotNull("Should have trace node before reset",
tracerService.getTraceNode());
+ assertNotNull(tracerService.getTraceNode(), "Should have trace node
before reset");
tracer.reset();
- assertNull("Should not have trace node after reset",
tracerService.getTraceNode());
+ assertNull(tracerService.getTraceNode(), "Should not have trace node
after reset");
// Verify that tracing is still enabled after reset
- assertTrue("Tracing should still be enabled after reset",
tracerService.isTracingEnabled());
+ assertTrue(tracerService.isTracingEnabled(), "Tracing should still be
enabled after reset");
}
@Test
@@ -181,9 +181,9 @@ public class DefaultTracerServiceTest {
tracerService.cleanup();
RequestTracer newTracer = tracerService.getCurrentTracer();
- assertNotSame("Should get new tracer instance after cleanup",
originalTracer, newTracer);
- assertFalse("New tracer should be disabled", newTracer.isEnabled());
- assertNull("New tracer should have no trace node",
tracerService.getTraceNode());
+ assertNotSame(originalTracer, newTracer, "Should get new tracer
instance after cleanup");
+ assertFalse(newTracer.isEnabled(), "New tracer should be disabled");
+ assertNull(tracerService.getTraceNode(), "New tracer should have no
trace node");
}
@Test
@@ -210,9 +210,9 @@ public class DefaultTracerServiceTest {
// Verify this thread's trace
TraceNode node = tracer.getTraceNode();
- assertNotNull("Thread " + threadId + " should have a
trace node", node);
- assertEquals("Thread " + threadId + " should have
correct operation type",
- "thread-" + threadId, node.getOperationType());
+ assertNotNull(node, "Thread " + threadId + " should
have a trace node");
+ assertEquals("thread-" + threadId,
node.getOperationType(),
+ "Thread " + threadId + " should have correct
operation type");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
@@ -227,8 +227,8 @@ public class DefaultTracerServiceTest {
startLatch.countDown();
// Wait for all threads to complete
- assertTrue("All threads should complete within timeout",
- completionLatch.await(TIMEOUT_SECONDS, TimeUnit.SECONDS));
+ assertTrue(completionLatch.await(TIMEOUT_SECONDS,
TimeUnit.SECONDS),
+ "All threads should complete within timeout");
} finally {
executor.shutdown();
if (!executor.awaitTermination(TIMEOUT_SECONDS, TimeUnit.SECONDS))
{
@@ -249,10 +249,33 @@ public class DefaultTracerServiceTest {
tracer.endOperation(null, null);
TraceNode node = tracerService.getTraceNode();
- assertNotNull("Node should exist even with null values", node);
- assertNull("Operation type should be null", node.getOperationType());
- assertNull("Description should be null", node.getDescription());
- assertNull("Context should be null", node.getContext());
- assertNull("Result should be null", node.getResult());
+ assertNotNull(node, "Node should exist even with null values");
+ assertNull(node.getOperationType(), "Operation type should be null");
+ assertNull(node.getDescription(), "Description should be null");
+ assertNull(node.getContext(), "Context should be null");
+ assertNull(node.getResult(), "Result should be null");
+ }
+
+ @Test
+ public void testTraceShouldNotFailWhenContextToStringOverflowsStack() {
+ tracerService.enableTracing();
+ RequestTracer tracer = tracerService.getCurrentTracer();
+
+ tracer.startOperation("test", "Root operation", null);
+ Object badContext = new Object() {
+ @Override
+ public String toString() {
+ return toString();
+ }
+ };
+
+ assertDoesNotThrow(() -> tracer.trace("Test with bad context",
badContext),
+ "Tracer.trace should not throw even if context.toString
overflows the stack");
+
+ TraceNode rootNode = tracerService.getTraceNode();
+ assertNotNull(rootNode, "Root node should be created");
+ assertEquals(1, rootNode.getTraces().size(), "Trace should be recorded
even when context rendering fails");
+ assertTrue(rootNode.getTraces().get(0).contains("StackOverflowError"),
+ "Trace should contain a StackOverflowError marker when context
rendering overflows");
}
}
\ No newline at end of file