From 3ce73e78198edb511e0313c1b9f2f84cdcb106a0 Mon Sep 17 00:00:00 2001 From: Serge Huber Date: Tue, 5 May 2026 17:17:56 +0200 Subject: [PATCH 01/21] UNOMI-920 Improve toString() methods with YAML formatting for debugging - Add YamlUtils (SnakeYaml) with YamlConvertible, YamlMapBuilder, circular reference detection (identity-based visited sets), and recursion depth limits. - Implement YAML-backed toString()/toYaml() on core API types extending Item / MetadataItem (Condition, ConditionType, Action, ActionType, Rule, Segment, Goal, Scoring, ScoringElement, Parameter, Metadata, etc.). - Add YamlUtilsTest coverage for builder, toYamlValue, and representative rules. Build and integration alignment: - unomi-api: snakeyaml + test dependencies; manage mockito-core version in BOM. - RESTParameter: use Object for defaultValue to match Parameter#getDefaultValue(). - RulesServiceImpl: avoid NPE when tracked parameter defaultValue is null before split. - itests: override awaitility to 3.1.6 for OSGi (Hamcrest 1.3 bundle); Karaf itests common logback exclusions; hamcrest bundle scope test. --- api/pom.xml | 22 + .../main/java/org/apache/unomi/api/Item.java | 62 +- .../java/org/apache/unomi/api/Metadata.java | 46 +- .../org/apache/unomi/api/MetadataItem.java | 57 +- .../java/org/apache/unomi/api/Parameter.java | 64 +- .../org/apache/unomi/api/actions/Action.java | 48 +- .../apache/unomi/api/actions/ActionType.java | 40 +- .../unomi/api/conditions/Condition.java | 113 +++- .../unomi/api/conditions/ConditionType.java | 43 +- .../java/org/apache/unomi/api/goals/Goal.java | 44 +- .../java/org/apache/unomi/api/rules/Rule.java | 46 +- .../apache/unomi/api/segments/Scoring.java | 38 +- .../unomi/api/segments/ScoringElement.java | 47 +- .../apache/unomi/api/segments/Segment.java | 40 +- .../org/apache/unomi/api/utils/YamlUtils.java | 319 +++++++++ .../apache/unomi/api/utils/YamlUtilsTest.java | 610 ++++++++++++++++++ bom/pom.xml | 20 + itests/pom.xml | 22 +- pom.xml | 4 + .../unomi/rest/models/RESTParameter.java | 6 +- .../services/impl/rules/RulesServiceImpl.java | 10 +- 21 files changed, 1656 insertions(+), 45 deletions(-) create mode 100644 api/src/main/java/org/apache/unomi/api/utils/YamlUtils.java create mode 100644 api/src/test/java/org/apache/unomi/api/utils/YamlUtilsTest.java diff --git a/api/pom.xml b/api/pom.xml index af8c962206..4d9ed7fbd0 100644 --- a/api/pom.xml +++ b/api/pom.xml @@ -73,6 +73,28 @@ jackson-databind provided + + org.yaml + snakeyaml + + + + + junit + junit + test + + + org.mockito + mockito-core + test + + + + org.slf4j + slf4j-simple + test + 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 de283ebe9a..9836cf40df 100644 --- a/api/src/main/java/org/apache/unomi/api/Item.java +++ b/api/src/main/java/org/apache/unomi/api/Item.java @@ -17,14 +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.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 must define a @@ -36,10 +40,10 @@ * 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 = 7446061538573517071L; + private static final long serialVersionUID = 1217180125083162915L; private static final Map itemTypeCache = new ConcurrentHashMap<>(); @@ -150,4 +154,54 @@ public Object getSystemMetadata(String key) { public void setSystemMetadata(String key, Object value) { systemMetadata.put(key, value); } + + /** + * 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 toYaml(Set visited, int maxDepth) { + if (maxDepth <= 0) { + return YamlMapBuilder.create() + .put("itemId", itemId) + .put("itemType", itemType) + .put("systemMetadata", "") + .build(); + } + final Set 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) + .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 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 9a112e7664..ec25c7fa89 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, Serializable { +public class Metadata implements Comparable, Serializable, YamlConvertible { private static final long serialVersionUID = 7446061538573517071L; @@ -279,5 +287,41 @@ public int hashCode() { 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 toYaml(Set visited, int maxDepth) { + if (visited != null && visited.contains(this)) { + return circularRef(); + } + final Set 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() { + Map 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 78a419863a..35ef596a25 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}. @@ -31,7 +39,7 @@ public MetadataItem() { } public MetadataItem(Metadata metadata) { - super(metadata.getId()); + super(metadata != null ? metadata.getId() : null); this.metadata = metadata; } @@ -54,7 +62,52 @@ public void setMetadata(Metadata metadata) { @XmlTransient public String getScope() { - return metadata.getScope(); + if (metadata != null) { + return metadata.getScope(); + } + 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 toYaml(Set visited, int maxDepth) { + if (maxDepth <= 0) { + return YamlMapBuilder.create() + .put("metadata", "") + .build(); + } + final Set 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() { + Map 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 4833c5a5f1..425d86280c 100644 --- a/api/src/main/java/org/apache/unomi/api/Parameter.java +++ b/api/src/main/java/org/apache/unomi/api/Parameter.java @@ -17,20 +17,27 @@ package org.apache.unomi.api; +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 = 7446061538573517071L; + private static final long serialVersionUID = 6019392686888941547L; - String id; - String type; - boolean multivalued = false; - String defaultValue = null; + private String id; + private String type; + private boolean multivalued; + private Object defaultValue; public Parameter() { } @@ -45,14 +52,26 @@ public String getId() { return id; } + public void setId(String id) { + this.id = id; + } + public String getType() { return type; } + public void setType(String type) { + this.type = type; + } + public boolean isMultivalued() { return multivalued; } + public void setMultivalued(boolean multivalued) { + this.multivalued = multivalued; + } + /** * @param choiceListInitializerFilter a reference to a choicelist * @deprecated As of version 1.1.0-incubating @@ -62,11 +81,40 @@ public void setChoiceListInitializerFilter(String choiceListInitializerFilter) { // Avoid errors when deploying old definitions } - public String getDefaultValue() { + public Object getDefaultValue() { return defaultValue; } - public void setDefaultValue(String defaultValue) { + public void setDefaultValue(Object defaultValue) { this.defaultValue = defaultValue; } + + /** + * 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 toYaml(Set visited, int maxDepth) { + if (maxDepth <= 0) { + return YamlUtils.YamlMapBuilder.create() + .put("id", id) + .put("validation", "") + .build(); + } + return YamlUtils.YamlMapBuilder.create() + .putIfNotNull("id", id) + .putIfNotNull("type", type) + .putIf("multivalued", true, multivalued) + .putIfNotNull("defaultValue", defaultValue) + .build(); + } + + @Override + public String toString() { + 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 b3505bbe82..29c6927e1c 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 parameterValues = new HashMap<>(); @@ -117,4 +125,42 @@ public void setParameter(String name, Object value) { 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 toYaml(Set visited, int maxDepth) { + if (maxDepth <= 0) { + return YamlMapBuilder.create() + .put("type", actionTypeId != null ? actionTypeId : "Action") + .put("parameterValues", "") + .build(); + } + if (visited != null && visited.contains(this)) { + return circularRef(); + } + final Set 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 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 5ff336d9ff..e170c779b5 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 @@ -20,14 +20,18 @@ import org.apache.unomi.api.Metadata; import org.apache.unomi.api.MetadataItem; import org.apache.unomi.api.Parameter; +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 { +public class ActionType extends MetadataItem implements YamlConvertible { public static final String ITEM_TYPE = "actionType"; private static final long serialVersionUID = -3522958600710010935L; @@ -101,4 +105,34 @@ public boolean equals(Object o) { public int hashCode() { return itemId.hashCode(); } + + /** + * 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 toYaml(Set visited, int maxDepth) { + if (maxDepth <= 0) { + return YamlMapBuilder.create() + .put("parameters", "") + .build(); + } + if (visited != null && visited.contains(this)) { + return circularRef(); + } + final Set 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) + .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 526ff463f7..812ee58628 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 @@ -17,16 +17,22 @@ 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.Map; +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; @@ -65,7 +71,9 @@ public ConditionType getConditionType() { */ public void setConditionType(ConditionType conditionType) { this.conditionType = conditionType; - this.conditionTypeId = conditionType.getItemId(); + if (conditionType != null) { + this.conditionTypeId = conditionType.getItemId(); + } } /** @@ -123,7 +131,7 @@ public boolean containsParameter(String name) { * @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; } /** @@ -156,13 +164,96 @@ public int hashCode() { return result; } + /** + * 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 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 + */ + @Override + public Map toYaml(Set visited, int maxDepth) { + if (maxDepth <= 0) { + return YamlMapBuilder.create() + .put("type", conditionTypeId != null ? conditionTypeId : "Condition") + .put("parameterValues", "") + .build(); + } + if (visited != null && visited.contains(this)) { + return circularRef(); + } + final Set 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()) { + builder.put("parameterValues", toYamlValue(parameterValues, visitedSet, maxDepth - 1)); + } + return builder.build(); + } finally { + visitedSet.remove(this); + } + } + + /** + * 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); + } + + // Deep copy parameter values + Map copiedParams = new HashMap<>(); + if (this.parameterValues != null) { + for (Map.Entry 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 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() { - final StringBuilder sb = new StringBuilder("Condition{"); - sb.append("conditionType=").append(conditionType); - sb.append(", conditionTypeId='").append(conditionTypeId).append('\''); - sb.append(", parameterValues=").append(parameterValues); - sb.append('}'); - return sb.toString(); + Map 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 c8a43225c1..e8af9c41fc 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 @@ -20,10 +20,14 @@ import org.apache.unomi.api.Metadata; import org.apache.unomi.api.MetadataItem; import org.apache.unomi.api.Parameter; +import org.apache.unomi.api.utils.YamlUtils.YamlConvertible; +import org.apache.unomi.api.utils.YamlUtils.YamlMapBuilder; import javax.xml.bind.annotation.XmlElement; -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; /** * 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 @@ -31,7 +35,7 @@ * 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 { +public class ConditionType extends MetadataItem implements YamlConvertible { public static final String ITEM_TYPE = "conditionType"; private static final long serialVersionUID = -6965481691241954969L; @@ -142,4 +146,37 @@ public boolean equals(Object o) { public int hashCode() { return itemId.hashCode(); } + + /** + * Converts this condition 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 condition type + */ + @Override + public Map toYaml(Set visited, int maxDepth) { + if (maxDepth <= 0) { + return YamlMapBuilder.create() + .put("parentCondition", "") + .put("parameters", "") + .build(); + } + if (visited != null && visited.contains(this)) { + return circularRef(); + } + final Set visitedSet = visited != null ? visited : new HashSet<>(); + visitedSet.add(this); + try { + return YamlMapBuilder.create() + .mergeObject(super.toYaml(visitedSet, maxDepth)) + .putIfNotNull("conditionEvaluator", conditionEvaluator) + .putIfNotNull("queryBuilder", queryBuilder) + .putIfNotNull("parentCondition", parentCondition != null ? toYamlValue(parentCondition, visitedSet, maxDepth - 1) : null) + .putIfNotEmpty("parameters", parameters != null ? (Collection) toYamlValue(parameters, visitedSet, maxDepth - 1) : null) + .build(); + } finally { + visitedSet.remove(this); + } + } } 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 1b2ff0e88f..12694b3d7e 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.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 @@ *
  • audience filtering: any visitor is considered for scope-level goals while campaign-level goals only consider visitors who match the campaign's conditions * */ -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 String getCampaignId() { 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 toYaml(Set visited, int maxDepth) { + if (maxDepth <= 0) { + return YamlMapBuilder.create() + .put("startEvent", "") + .put("targetEvent", "") + .put("campaignId", campaignId) + .build(); + } + if (visited != null && visited.contains(this)) { + return circularRef(); + } + final Set 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 0aedfb1238..63750c0d44 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 @@ 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 @@ * 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 int getPriority() { 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 toYaml(Set visited, int maxDepth) { + if (maxDepth <= 0) { + return YamlMapBuilder.create() + .put("condition", "") + .put("actions", "") + .put("priority", priority) + .build(); + } + if (visited != null && visited.contains(this)) { + return circularRef(); + } + final Set 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 6018fb376c..3d82e6e7e0 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.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 void setElements(List elements) { 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 toYaml(Set visited, int maxDepth) { + if (maxDepth <= 0) { + return YamlMapBuilder.create() + .put("elements", "") + .build(); + } + if (visited != null && visited.contains(this)) { + return circularRef(); + } + final Set 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 1af5147fbe..bfc5a21ccb 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 int getValue() { 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 toYaml(Set visited, int maxDepth) { + if (maxDepth <= 0) { + return YamlMapBuilder.create() + .put("condition", "") + .put("value", value) + .build(); + } + if (visited != null && visited.contains(this)) { + return circularRef(); + } + final Set 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 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 4e0d338306..2729ec74d0 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.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 void setCondition(Condition condition) { 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 toYaml(Set visited, int maxDepth) { + if (maxDepth <= 0) { + return YamlMapBuilder.create() + .put("condition", "") + .build(); + } + if (visited != null && visited.contains(this)) { + return circularRef(); + } + final Set 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 new file mode 100644 index 0000000000..664f63fb79 --- /dev/null +++ b/api/src/main/java/org/apache/unomi/api/utils/YamlUtils.java @@ -0,0 +1,319 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.unomi.api.utils; + +import org.yaml.snakeyaml.DumperOptions; +import org.yaml.snakeyaml.Yaml; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** + * YAML utilities using SnakeYaml with fluent API wrapper. + * Provides utilities for building YAML structures and formatting them via SnakeYaml. + */ +public class YamlUtils { + // SnakeYaml instance with configured options + private static final Yaml YAML_INSTANCE; + + static { + DumperOptions options = new DumperOptions(); + options.setIndent(2); + options.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK); + options.setPrettyFlow(true); + options.setDefaultScalarStyle(DumperOptions.ScalarStyle.PLAIN); + YAML_INSTANCE = new Yaml(options); + } + + /** + * 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 toYaml(Set 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 + */ + default Map toYaml(Set 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 toYaml() { + return toYaml(null, 20); + } + } + + /** + * Fluent builder for creating YAML Map structures. + * Provides chaining methods to avoid repeating the map variable. + */ + public static class YamlMapBuilder { + private final Map map; + + private YamlMapBuilder() { + this.map = new LinkedHashMap<>(); + } + + /** + * Creates a new builder instance. + * + * @return a new YamlMapBuilder + */ + public static YamlMapBuilder create() { + return new YamlMapBuilder(); + } + + /** + * Adds a field if the value is not null. + * + * @param key the key (must not be null) + * @param value the value (only added if not null) + * @return this builder for chaining + * @throws NullPointerException if key is null + */ + public YamlMapBuilder putIfNotNull(String key, Object value) { + if (key == null) { + throw new NullPointerException("Key must not be null"); + } + if (value != null) { + map.put(key, value); + } + return this; + } + + /** + * Adds a field if the condition is true. + * + * @param key the key (must not be null) + * @param value the value (only added if condition is true) + * @param condition the condition + * @return this builder for chaining + * @throws NullPointerException if key is null + */ + public YamlMapBuilder putIf(String key, Object value, boolean condition) { + if (key == null) { + throw new NullPointerException("Key must not be null"); + } + if (condition) { + map.put(key, value); + } + return this; + } + + /** + * Adds a field unconditionally. + * + * @param key the key (must not be null) + * @param value the value + * @return this builder for chaining + * @throws NullPointerException if key is null + */ + public YamlMapBuilder put(String key, Object value) { + if (key == null) { + throw new NullPointerException("Key must not be null"); + } + map.put(key, value); + return this; + } + + /** + * Adds a field if the collection is not null and not empty. + * + * @param key the key (must not be null) + * @param collection the collection (only added if not null and not empty) + * @return this builder for chaining + * @throws NullPointerException if key is null + */ + public YamlMapBuilder putIfNotEmpty(String key, java.util.Collection collection) { + if (key == null) { + throw new NullPointerException("Key must not be null"); + } + if (collection != null && !collection.isEmpty()) { + map.put(key, collection); + } + 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: + *
    +         * return YamlMapBuilder.create()
    +         *     .mergeObject(super.toYaml(visitedSet))
    +         *     .putIfNotNull("field", value)
    +         *     .build();
    +         * 
    + * + * @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 objectMap) { + if (objectMap != null) { + objectMap.forEach(map::put); + } + return this; + } + + /** + * Builds and returns a defensive copy of the map. + * + * @return a new LinkedHashMap containing the built entries + */ + public Map build() { + return new LinkedHashMap<>(map); + } + } + + /** + * Converts a Set to a sorted List for YAML output. + * + * @param set the set to convert + * @return a sorted list, or null if the set is null or empty + */ + public static > List setToSortedList(Set set) { + if (set == null || set.isEmpty()) { + return null; + } + return set.stream().sorted().collect(Collectors.toList()); + } + + /** + * Converts a Set to a sorted List using a mapper function. + * + * @param set the set to convert + * @param mapper the mapper function (must not be null) + * @return a sorted list, or null if the set is null or empty + * @throws NullPointerException if mapper is null + */ + public static > List setToSortedList(Set set, Function mapper) { + if (mapper == null) { + throw new NullPointerException("Mapper function must not be null"); + } + if (set == null || set.isEmpty()) { + return null; + } + return set.stream().map(mapper).sorted().collect(Collectors.toList()); + } + + /** + * Converts a value to YAML-compatible format, handling nested structures. + * 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) + * @return the converted value + */ + public static Object toYamlValue(Object value, Set 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 visited, int maxDepth) { + if (maxDepth <= 0) { + return ""; + } + if (value == null) { + return null; + } + if (value instanceof YamlConvertible) { + // 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 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, maxDepth - 1)) + .collect(Collectors.toList()); + } + if (value instanceof Map) { + Map inputMap = (Map) value; + Map result = new LinkedHashMap<>(); + + 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. + * + * @param value the value to format + * @return YAML string representation + */ + public static String format(Object value) { + return YAML_INSTANCE.dump(value); + } + + /** + * Creates a circular reference marker map. + * + * @return a map indicating a circular reference + */ + public static Map circularRef() { + return YamlMapBuilder.create() + .put("$ref", "circular") + .build(); + } +} 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 new file mode 100644 index 0000000000..c40c066ca6 --- /dev/null +++ b/api/src/test/java/org/apache/unomi/api/utils/YamlUtilsTest.java @@ -0,0 +1,610 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +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.*; + +import static org.junit.Assert.*; + +/** + * Unit tests for YamlUtils fluent API. + * Tests focus on our fluent API, not SnakeYaml's implementation. + */ +public class YamlUtilsTest { + + @Test + public void testYamlMapBuilderCreate() { + YamlUtils.YamlMapBuilder builder = YamlUtils.YamlMapBuilder.create(); + assertNotNull("Builder should be created", builder); + } + + @Test + public void testYamlMapBuilderPut() { + Map map = YamlUtils.YamlMapBuilder.create() + .put("key1", "value1") + .put("key2", 42) + .build(); + assertEquals("First value should be set", "value1", map.get("key1")); + assertEquals("Second value should be set", 42, map.get("key2")); + } + + @Test + public void testYamlMapBuilderPutIfNotNull() { + Map map = YamlUtils.YamlMapBuilder.create() + .putIfNotNull("key1", "value1") + .putIfNotNull("key2", null) + .putIfNotNull("key3", "value3") + .build(); + assertEquals("Non-null value should be set", "value1", map.get("key1")); + assertFalse("Null value should not be set", map.containsKey("key2")); + assertEquals("Another non-null value should be set", "value3", map.get("key3")); + } + + @Test + public void testYamlMapBuilderPutIf() { + Map map = YamlUtils.YamlMapBuilder.create() + .putIf("key1", "value1", true) + .putIf("key2", "value2", false) + .putIf("key3", "value3", true) + .build(); + assertEquals("Value with true condition should be set", "value1", map.get("key1")); + assertFalse("Value with false condition should not be set", map.containsKey("key2")); + assertEquals("Another value with true condition should be set", "value3", map.get("key3")); + } + + @Test + public void testYamlMapBuilderPutIfNotEmpty() { + Map map = YamlUtils.YamlMapBuilder.create() + .putIfNotEmpty("key1", Arrays.asList("a", "b")) + .putIfNotEmpty("key2", Collections.emptyList()) + .putIfNotEmpty("key3", null) + .putIfNotEmpty("key4", Arrays.asList("c")) + .build(); + assertTrue("Non-empty collection should be set", map.containsKey("key1")); + assertFalse("Empty collection should not be set", map.containsKey("key2")); + assertFalse("Null collection should not be set", map.containsKey("key3")); + assertTrue("Another non-empty collection should be set", map.containsKey("key4")); + } + + @Test + public void testYamlMapBuilderChaining() { + Map map = YamlUtils.YamlMapBuilder.create() + .put("a", 1) + .putIfNotNull("b", "value") + .putIf("c", 3, true) + .putIfNotEmpty("d", Arrays.asList(1, 2)) + .build(); + assertEquals("All valid entries should be added", 4, map.size()); + } + + @Test + public void testYamlMapBuilderNullKeyThrowsException() { + try { + YamlUtils.YamlMapBuilder.create().put(null, "value"); + fail("Null key should throw NullPointerException"); + } catch (NullPointerException e) { + // Expected + } + } + + @Test + public void testYamlMapBuilderNullKeyInPutIfNotNull() { + try { + YamlUtils.YamlMapBuilder.create().putIfNotNull(null, "value"); + fail("Null key in putIfNotNull should throw NullPointerException"); + } catch (NullPointerException e) { + // Expected + } + } + + @Test + public void testYamlMapBuilderNullKeyInPutIf() { + try { + YamlUtils.YamlMapBuilder.create().putIf(null, "value", true); + fail("Null key in putIf should throw NullPointerException"); + } catch (NullPointerException e) { + // Expected + } + } + + @Test + public void testYamlMapBuilderNullKeyInPutIfNotEmpty() { + try { + YamlUtils.YamlMapBuilder.create().putIfNotEmpty(null, Arrays.asList(1)); + fail("Null key in putIfNotEmpty should throw NullPointerException"); + } catch (NullPointerException e) { + // Expected + } + } + + @Test + public void testYamlMapBuilderBuildReturnsNewMap() { + YamlUtils.YamlMapBuilder builder = YamlUtils.YamlMapBuilder.create(); + builder.put("key", "value"); + Map map1 = builder.build(); + Map map2 = builder.build(); + assertNotSame("Each build() should return a new map", map1, map2); + assertEquals("Both maps should have same content", map1, map2); + } + + @Test + public void testSetToSortedList() { + Set set = new LinkedHashSet<>(Arrays.asList("zebra", "apple", "banana")); + List result = YamlUtils.setToSortedList(set); + assertNotNull("Result should not be null", result); + assertEquals("Set should be converted to sorted list", Arrays.asList("apple", "banana", "zebra"), result); + } + + @Test + public void testSetToSortedListNull() { + List result = YamlUtils.setToSortedList((Set) null); + assertNull("Null set should return null", result); + } + + @Test + public void testSetToSortedListEmpty() { + List result = YamlUtils.setToSortedList(Collections.emptySet()); + assertNull("Empty set should return null", result); + } + + @Test + public void testSetToSortedListWithMapper() { + Set set = new LinkedHashSet<>(Arrays.asList(3, 1, 2)); + List result = YamlUtils.setToSortedList(set, String::valueOf); + assertNotNull("Result should not be null", result); + assertEquals("Set should be converted to sorted list using mapper", Arrays.asList("1", "2", "3"), result); + } + + @Test + public void testSetToSortedListWithMapperNull() { + try { + YamlUtils.setToSortedList(Collections.singleton(1), null); + fail("Null mapper should throw NullPointerException"); + } catch (NullPointerException e) { + // Expected + } + } + + @Test + public void testSetToSortedListWithMapperNullSet() { + List result = YamlUtils.setToSortedList(null, String::valueOf); + assertNull("Null set should return null even with mapper", result); + } + + @Test + public void testToYamlValueWithYamlConvertible() { + YamlUtils.YamlConvertible convertible = (visited, maxDepth) -> { + Map map = new LinkedHashMap<>(); + map.put("test", "value"); + return map; + }; + Set visited = new HashSet<>(); + Object result = YamlUtils.toYamlValue(convertible, visited); + assertTrue("YamlConvertible should be converted to Map", result instanceof Map); + Map map = (Map) result; + assertEquals("Converted map should contain test value", "value", map.get("test")); + } + + @Test + public void testToYamlValueWithList() { + List list = Arrays.asList("a", "b", "c"); + Set visited = new HashSet<>(); + Object result = YamlUtils.toYamlValue(list, visited); + assertTrue("List should remain a List", result instanceof List); + assertEquals("List should be unchanged", list, result); + } + + @Test + public void testToYamlValueWithMap() { + Map map = new LinkedHashMap<>(); + map.put("key", "value"); + Set visited = new HashSet<>(); + Object result = YamlUtils.toYamlValue(map, visited); + assertTrue("Map should remain a Map", result instanceof Map); + assertEquals("Map should contain key-value", "value", ((Map) result).get("key")); + } + + @Test + public void testToYamlValueWithNull() { + Set visited = new HashSet<>(); + Object result = YamlUtils.toYamlValue(null, visited); + assertNull("Null should return null", result); + } + + @Test + public void testToYamlValueWithPrimitive() { + Set visited = new HashSet<>(); + Object result = YamlUtils.toYamlValue(42, visited); + assertEquals("Primitive should remain unchanged", 42, result); + } + + @Test + public void testCircularRef() { + Map result = YamlUtils.circularRef(); + assertNotNull("circularRef should return a map", result); + assertEquals("Should contain $ref: circular", "circular", result.get("$ref")); + assertEquals("Should have only one entry", 1, result.size()); + } + + @Test + public void testFormatBasic() { + // Just verify format() works - we don't test SnakeYaml's output format + Map map = new LinkedHashMap<>(); + map.put("key", "value"); + String result = YamlUtils.format(map); + assertNotNull("Format should return a string", result); + 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 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 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 conditionMap = (Map) result.get("condition"); + assertNotNull("Condition should be serialized", conditionMap); + Map paramValues = (Map) conditionMap.get("parameterValues"); + assertNotNull("Parameter values should exist", paramValues); + Map circularRef = (Map) 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 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 actionMap = (Map) actions.get(0); + Map paramValues = (Map) actionMap.get("parameterValues"); + assertNotNull("Parameter values should exist", paramValues); + Map circularRef = (Map) 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 result = rule.toYaml(null); + assertNotNull("Rule should serialize to YAML", result); + + // Navigate through the nested structure + Map conditionMap = (Map) result.get("condition"); + Map paramValues = (Map) conditionMap.get("parameterValues"); + Map nestedConditionMap = (Map) paramValues.get("nested"); + Map nestedParamValues = (Map) nestedConditionMap.get("parameterValues"); + Map circularRef = (Map) 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 result = rule.toYaml(null); + Map conditionMap = (Map) result.get("condition"); + Map paramValues = (Map) conditionMap.get("parameterValues"); + + // All three references should show circular ref + for (String key : Arrays.asList("rule1", "rule2", "rule3")) { + Map circularRef = (Map) 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 result = rule.toYaml(null); + Map conditionMap = (Map) result.get("condition"); + Map paramValues = (Map) 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 circularRef1 = (Map) 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 circularRef2 = (Map) 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 nestedMap = new HashMap<>(); + nestedMap.put("level1", new HashMap() {{ + put("level2", new HashMap() {{ + put("rule", rule); + }}); + }}); + condition.getParameterValues().put("nested", nestedMap); + rule.setCondition(condition); + + Map result = rule.toYaml(null); + Map conditionMap = (Map) result.get("condition"); + Map paramValues = (Map) conditionMap.get("parameterValues"); + Map nested = (Map) paramValues.get("nested"); + Map level1 = (Map) nested.get("level1"); + Map level2 = (Map) level1.get("level2"); + Map circularRef = (Map) 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 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 conditionMap = (Map) 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 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 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 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 result1 = rule1.toYaml(null); + assertNotNull("Rule1 should serialize", result1); + + Map conditionMap1 = (Map) result1.get("condition"); + Map paramValues1 = (Map) conditionMap1.get("parameterValues"); + Map rule2Ref = (Map) 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 conditionMap2 = (Map) rule2Ref.get("condition"); + assertNotNull("Rule2's condition should exist", conditionMap2); + Map paramValues2 = (Map) conditionMap2.get("parameterValues"); + Map rule1CircularRef = (Map) 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/bom/pom.xml b/bom/pom.xml index 69cb5a0518..17b0f37529 100644 --- a/bom/pom.xml +++ b/bom/pom.xml @@ -503,6 +503,26 @@ junit ${junit.version} + + org.junit.jupiter + junit-jupiter + ${junit-jupiter.version} + + + org.mockito + mockito-core + ${mockito.version} + + + org.mockito + mockito-junit-jupiter + ${mockito.version} + + + org.awaitility + awaitility + ${awaitility.version} + diff --git a/itests/pom.xml b/itests/pom.xml index 98e98e9e56..d8b5d4a5ed 100644 --- a/itests/pom.xml +++ b/itests/pom.xml @@ -32,6 +32,8 @@ elasticsearch false itests-opensearch + + true @@ -43,6 +45,14 @@ pom import + + + + + org.awaitility + awaitility + 3.1.6 + @@ -53,6 +63,16 @@ common ${karaf.version} test + + + ch.qos.logback + logback-classic + + + ch.qos.logback + logback-core + + @@ -105,7 +125,7 @@ org.apache.servicemix.bundles org.apache.servicemix.bundles.hamcrest 1.3_1 - runtime + test org.apache.httpcomponents diff --git a/pom.xml b/pom.xml index 25360797d5..d4825e7ec3 100644 --- a/pom.xml +++ b/pom.xml @@ -80,6 +80,7 @@ 2.3 3.10 2.19.0 + 1.7.36 9.12.2 2.4.0 2.12.7 @@ -102,6 +103,9 @@ 4.5.14 4.4.16 4.13.2 + 5.8.2 + 4.5.1 + 4.2.0 2.2.1 4.3.4 1.6.0 diff --git a/rest/src/main/java/org/apache/unomi/rest/models/RESTParameter.java b/rest/src/main/java/org/apache/unomi/rest/models/RESTParameter.java index bc7ab9acf2..d6257c6f09 100644 --- a/rest/src/main/java/org/apache/unomi/rest/models/RESTParameter.java +++ b/rest/src/main/java/org/apache/unomi/rest/models/RESTParameter.java @@ -26,7 +26,7 @@ public class RESTParameter { private String id; private String type; private boolean multivalued = false; - private String defaultValue = null; + private Object defaultValue = null; public String getId() { return id; @@ -52,11 +52,11 @@ public void setMultivalued(boolean multivalued) { this.multivalued = multivalued; } - public String getDefaultValue() { + public Object getDefaultValue() { return defaultValue; } - public void setDefaultValue(String defaultValue) { + public void setDefaultValue(Object defaultValue) { this.defaultValue = defaultValue; } diff --git a/services/src/main/java/org/apache/unomi/services/impl/rules/RulesServiceImpl.java b/services/src/main/java/org/apache/unomi/services/impl/rules/RulesServiceImpl.java index fe3fe8fb02..8b6e7063c9 100644 --- a/services/src/main/java/org/apache/unomi/services/impl/rules/RulesServiceImpl.java +++ b/services/src/main/java/org/apache/unomi/services/impl/rules/RulesServiceImpl.java @@ -450,7 +450,15 @@ public Set getTrackedConditions(Item source) { trackedCondition.getConditionType().getParameters().forEach(parameter -> { try { if (TRACKED_PARAMETER.equals(parameter.getId())) { - Arrays.stream(StringUtils.split(parameter.getDefaultValue(), ",")).forEach(trackedParameter -> { + // Parameter#getDefaultValue is Object; null must not call toString() (NPE) or be passed to split. + Object defaultValue = parameter.getDefaultValue(); + if (defaultValue == null) { + LOGGER.debug( + "Skipping tracked parameter mapping: parameter id={} has null defaultValue for condition type {}", + parameter.getId(), trackedCondition.getConditionType().getItemId()); + return; + } + Arrays.stream(StringUtils.split(defaultValue.toString(), ",")).forEach(trackedParameter -> { String[] param = StringUtils.split(StringUtils.trim(trackedParameter), ":"); trackedParameters.put(StringUtils.trim(param[1]), trackedCondition.getParameter(StringUtils.trim(param[0]))); }); From 94ec7a3595977d7e1117693049e41cf79d33e01e Mon Sep 17 00:00:00 2001 From: Serge Huber Date: Wed, 6 May 2026 06:17:46 +0200 Subject: [PATCH 02/21] UNOMI-879 Unified CRUD shell developer commands - Replace legacy per-type list/view commands with unified unomi:crud routing, shared bases (BaseCrudCommand, CrudCommand), completers, and type-specific CRUD implementations under org.apache.unomi.shell.dev. - Relocate deployment/tail/watch helpers into dev.commands package. - EventService/ProfileService: add deleteEvent/deleteSession; implementations delegate to PersistenceService.remove (aligned with unomi-3-dev). - BOM: dependency adjustments needed by shell-dev-commands build. --- .../unomi/api/services/EventService.java | 7 + .../unomi/api/services/ProfileService.java | 7 + bom/pom.xml | 6 + .../impl/events/EventServiceImpl.java | 5 + .../impl/profiles/ProfileServiceImpl.java | 5 + tools/shell-dev-commands/pom.xml | 13 + .../unomi/shell/commands/ActionList.java | 64 -- .../unomi/shell/commands/ActionView.java | 48 -- .../unomi/shell/commands/ConditionList.java | 64 -- .../unomi/shell/commands/ConditionView.java | 50 -- .../shell/commands/DeployDefinition.java | 113 ---- .../unomi/shell/commands/EventList.java | 90 --- .../unomi/shell/commands/EventSearch.java | 99 --- .../unomi/shell/commands/EventView.java | 60 -- .../unomi/shell/commands/ProfileList.java | 84 --- .../unomi/shell/commands/ProfileView.java | 48 -- .../apache/unomi/shell/commands/RuleList.java | 108 ---- .../unomi/shell/commands/SegmentList.java | 73 --- .../unomi/shell/commands/SegmentRemove.java | 56 -- .../unomi/shell/commands/SegmentView.java | 48 -- .../unomi/shell/commands/SessionList.java | 79 --- .../shell/commands/UndeployDefinition.java | 111 ---- .../shell/dev/actions/UnomiCrudCommand.java | 608 ++++++++++++++++++ .../unomi/shell/dev/commands/BaseCommand.java | 53 ++ .../shell/dev/commands/BaseListCommand.java | 74 +++ .../shell/dev/commands/BaseSimpleCommand.java | 61 ++ .../shell/dev/commands/CommandUtils.java | 80 +++ .../shell/dev/commands/DeployDefinition.java | 137 ++++ .../commands/DeploymentCommandSupport.java | 87 ++- .../shell/{ => dev}/commands/EventTail.java | 16 +- .../commands/ListCommandSupport.java | 15 +- .../dev/commands/RemoveCommandSupport.java | 51 ++ .../{ => dev}/commands/RuleResetStats.java | 7 +- .../shell/{ => dev}/commands/RuleTail.java | 16 +- .../shell/{ => dev}/commands/RuleWatch.java | 18 +- .../commands/TailCommandSupport.java | 12 +- .../shell/dev/commands/TailCommandUtils.java | 91 +++ .../dev/commands/UndeployDefinition.java | 97 +++ .../actions/ActionTypeCrudCommand.java | 157 +++++ .../campaigns/CampaignCrudCommand.java | 151 +++++ .../campaigns/CampaignEventCrudCommand.java | 143 ++++ .../conditions/ConditionTypeCrudCommand.java | 153 +++++ .../commands/consents/ConsentCrudCommand.java | 244 +++++++ .../dev/commands/events/EventCrudCommand.java | 167 +++++ .../dev/commands/goals/GoalCrudCommand.java | 141 ++++ .../commands/personas/PersonaCrudCommand.java | 150 +++++ .../profiles/ProfileAliasCrudCommand.java | 155 +++++ .../commands/profiles/ProfileCrudCommand.java | 183 ++++++ .../properties/PropertyTypeCrudCommand.java | 174 +++++ .../dev/commands/rules/RuleCrudCommand.java | 209 ++++++ .../rules/RuleStatisticsCrudCommand.java | 135 ++++ .../dev/commands/scopes/ScopeCrudCommand.java | 139 ++++ .../commands/scoring/ScoringCrudCommand.java | 142 ++++ .../commands/segments/SegmentCrudCommand.java | 195 ++++++ .../commands/sessions/SessionCrudCommand.java | 129 ++++ .../dev/commands/topics/TopicCrudCommand.java | 120 ++++ .../shell/dev/completers/BaseCompleter.java | 86 +++ .../shell/dev/completers/IdCompleter.java | 156 +++++ .../dev/completers/OperationCompleter.java | 37 ++ .../shell/dev/completers/TypeCompleter.java | 66 ++ .../shell/dev/services/BaseCrudCommand.java | 400 ++++++++++++ .../unomi/shell/dev/services/CrudCommand.java | 206 ++++++ 62 files changed, 5241 insertions(+), 1258 deletions(-) delete mode 100644 tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/ActionList.java delete mode 100644 tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/ActionView.java delete mode 100644 tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/ConditionList.java delete mode 100644 tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/ConditionView.java delete mode 100644 tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/DeployDefinition.java delete mode 100644 tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/EventList.java delete mode 100644 tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/EventSearch.java delete mode 100644 tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/EventView.java delete mode 100644 tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/ProfileList.java delete mode 100644 tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/ProfileView.java delete mode 100644 tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/RuleList.java delete mode 100644 tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/SegmentList.java delete mode 100644 tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/SegmentRemove.java delete mode 100644 tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/SegmentView.java delete mode 100644 tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/SessionList.java delete mode 100644 tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/UndeployDefinition.java create mode 100644 tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/actions/UnomiCrudCommand.java create mode 100644 tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/BaseCommand.java create mode 100644 tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/BaseListCommand.java create mode 100644 tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/BaseSimpleCommand.java create mode 100644 tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/CommandUtils.java create mode 100644 tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/DeployDefinition.java rename tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/{ => dev}/commands/DeploymentCommandSupport.java (72%) rename tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/{ => dev}/commands/EventTail.java (81%) rename tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/{ => dev}/commands/ListCommandSupport.java (87%) create mode 100644 tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/RemoveCommandSupport.java rename tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/{ => dev}/commands/RuleResetStats.java (86%) rename tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/{ => dev}/commands/RuleTail.java (80%) rename tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/{ => dev}/commands/RuleWatch.java (82%) rename tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/{ => dev}/commands/TailCommandSupport.java (93%) create mode 100644 tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/TailCommandUtils.java create mode 100644 tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/UndeployDefinition.java create mode 100644 tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/actions/ActionTypeCrudCommand.java create mode 100644 tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/campaigns/CampaignCrudCommand.java create mode 100644 tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/campaigns/CampaignEventCrudCommand.java create mode 100644 tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/conditions/ConditionTypeCrudCommand.java create mode 100644 tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/consents/ConsentCrudCommand.java create mode 100644 tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/events/EventCrudCommand.java create mode 100644 tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/goals/GoalCrudCommand.java create mode 100644 tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/personas/PersonaCrudCommand.java create mode 100644 tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/profiles/ProfileAliasCrudCommand.java create mode 100644 tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/profiles/ProfileCrudCommand.java create mode 100644 tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/properties/PropertyTypeCrudCommand.java create mode 100644 tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/rules/RuleCrudCommand.java create mode 100644 tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/rules/RuleStatisticsCrudCommand.java create mode 100644 tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/scopes/ScopeCrudCommand.java create mode 100644 tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/scoring/ScoringCrudCommand.java create mode 100644 tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/segments/SegmentCrudCommand.java create mode 100644 tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/sessions/SessionCrudCommand.java create mode 100644 tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/topics/TopicCrudCommand.java create mode 100644 tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/completers/BaseCompleter.java create mode 100644 tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/completers/IdCompleter.java create mode 100644 tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/completers/OperationCompleter.java create mode 100644 tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/completers/TypeCompleter.java create mode 100644 tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/services/BaseCrudCommand.java create mode 100644 tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/services/CrudCommand.java diff --git a/api/src/main/java/org/apache/unomi/api/services/EventService.java b/api/src/main/java/org/apache/unomi/api/services/EventService.java index fb6d60d70b..64ca1beebd 100644 --- a/api/src/main/java/org/apache/unomi/api/services/EventService.java +++ b/api/src/main/java/org/apache/unomi/api/services/EventService.java @@ -153,4 +153,11 @@ public interface EventService { * @param profileId identifier of the profile that we want to remove it's events */ void removeProfileEvents(String profileId); + + /** + * Deletes the event identified by the given identifier from persistence. + * + * @param eventIdentifier the unique identifier for the event + */ + void deleteEvent(String eventIdentifier); } diff --git a/api/src/main/java/org/apache/unomi/api/services/ProfileService.java b/api/src/main/java/org/apache/unomi/api/services/ProfileService.java index 03da6ce9be..bd4c537068 100644 --- a/api/src/main/java/org/apache/unomi/api/services/ProfileService.java +++ b/api/src/main/java/org/apache/unomi/api/services/ProfileService.java @@ -217,6 +217,13 @@ default Session loadSession(String sessionId) { */ void removeProfileSessions(String profileId); + /** + * Deletes the session identified by the given identifier from persistence. + * + * @param sessionIdentifier the unique identifier for the session + */ + void deleteSession(String sessionIdentifier); + /** * Checks whether the specified profile and/or session satisfy the specified condition. * diff --git a/bom/pom.xml b/bom/pom.xml index 17b0f37529..c63868276b 100644 --- a/bom/pom.xml +++ b/bom/pom.xml @@ -270,6 +270,12 @@ httpclient-osgi ${httpclient-osgi.version} + + org.apache.httpcomponents + httpclient-osgi + ${httpclient-osgi.version} + bundle + org.apache.kafka kafka-clients diff --git a/services/src/main/java/org/apache/unomi/services/impl/events/EventServiceImpl.java b/services/src/main/java/org/apache/unomi/services/impl/events/EventServiceImpl.java index f345466c7d..60680924e6 100644 --- a/services/src/main/java/org/apache/unomi/services/impl/events/EventServiceImpl.java +++ b/services/src/main/java/org/apache/unomi/services/impl/events/EventServiceImpl.java @@ -336,4 +336,9 @@ public void removeProfileEvents(String profileId){ persistenceService.removeByQuery(profileCondition,Event.class); } + + @Override + public void deleteEvent(String eventIdentifier) { + persistenceService.remove(eventIdentifier, Event.class); + } } diff --git a/services/src/main/java/org/apache/unomi/services/impl/profiles/ProfileServiceImpl.java b/services/src/main/java/org/apache/unomi/services/impl/profiles/ProfileServiceImpl.java index 9673a707cd..e6b74c9172 100644 --- a/services/src/main/java/org/apache/unomi/services/impl/profiles/ProfileServiceImpl.java +++ b/services/src/main/java/org/apache/unomi/services/impl/profiles/ProfileServiceImpl.java @@ -1015,6 +1015,11 @@ public void removeProfileSessions(String profileId) { persistenceService.removeByQuery(profileCondition, Session.class); } + @Override + public void deleteSession(String sessionIdentifier) { + persistenceService.remove(sessionIdentifier, Session.class); + } + @Override public boolean matchCondition(Condition condition, Profile profile, Session session) { ParserHelper.resolveConditionType(definitionsService, condition, "profile " + profile.getItemId() + " matching"); diff --git a/tools/shell-dev-commands/pom.xml b/tools/shell-dev-commands/pom.xml index 27b0b641f3..e62a637589 100644 --- a/tools/shell-dev-commands/pom.xml +++ b/tools/shell-dev-commands/pom.xml @@ -92,6 +92,7 @@ org.apache.httpcomponents httpclient-osgi + bundle provided @@ -99,6 +100,10 @@ commons-lang3 provided + + org.apache.commons + commons-csv + junit @@ -115,6 +120,14 @@ * + + org.apache.unomi.shell.dev.services, + org.apache.unomi.shell.dev.commands + + <_dsannotations>* + <_dsannotations-options>inherit + <_metatypeannotations>* + <_metatypeannotations-options>version;nested diff --git a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/ActionList.java b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/ActionList.java deleted file mode 100644 index 7839cb7c80..0000000000 --- a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/ActionList.java +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.unomi.shell.commands; - -import org.apache.commons.lang3.StringUtils; -import org.apache.karaf.shell.api.action.Command; -import org.apache.karaf.shell.api.action.lifecycle.Reference; -import org.apache.karaf.shell.api.action.lifecycle.Service; -import org.apache.unomi.api.actions.ActionType; -import org.apache.unomi.api.services.DefinitionsService; -import org.apache.unomi.common.DataTable; - -import java.util.ArrayList; -import java.util.Collection; - -@Command(scope = "unomi", name = "action-list", description = "This will list all the actions deployed in the Apache Unomi Context Server") -@Service -public class ActionList extends ListCommandSupport { - - @Reference - DefinitionsService definitionsService; - - @Override - protected String[] getHeaders() { - return new String[] { - "Id", - "Name", - "System tags" - }; - } - - @Override - protected DataTable buildDataTable() { - Collection allActions = definitionsService.getAllActionTypes(); - - DataTable dataTable = new DataTable(); - - for (ActionType actionType : allActions) { - ArrayList rowData = new ArrayList<>(); - rowData.add(actionType.getItemId()); - rowData.add(actionType.getMetadata().getName()); - rowData.add(StringUtils.join(actionType.getMetadata().getSystemTags(), ",")); - dataTable.addRow(rowData.toArray(new Comparable[rowData.size()])); - } - - dataTable.sort(new DataTable.SortCriteria(1, DataTable.SortOrder.ASCENDING)); - return dataTable; - } - -} diff --git a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/ActionView.java b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/ActionView.java deleted file mode 100644 index 939e7f9d1d..0000000000 --- a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/ActionView.java +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.unomi.shell.commands; - -import org.apache.karaf.shell.api.action.Action; -import org.apache.karaf.shell.api.action.Argument; -import org.apache.karaf.shell.api.action.Command; -import org.apache.karaf.shell.api.action.lifecycle.Reference; -import org.apache.karaf.shell.api.action.lifecycle.Service; -import org.apache.unomi.api.actions.ActionType; -import org.apache.unomi.api.services.DefinitionsService; -import org.apache.unomi.persistence.spi.CustomObjectMapper; - -@Command(scope = "unomi", name = "action-view", description = "This will display a single action deployed in the Apache Unomi Context Server") -@Service -public class ActionView implements Action { - - @Reference - DefinitionsService definitionsService; - - @Argument(index = 0, name = "actionId", description = "The identifier for the action", required = true, multiValued = false) - String actionTypeIdentifier; - - public Object execute() throws Exception { - ActionType actionType = definitionsService.getActionType(actionTypeIdentifier); - if (actionType == null) { - System.out.println("Couldn't find an action with id=" + actionTypeIdentifier); - return null; - } - String jsonRule = CustomObjectMapper.getObjectMapper().writerWithDefaultPrettyPrinter().writeValueAsString(actionType); - System.out.println(jsonRule); - return null; - } -} diff --git a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/ConditionList.java b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/ConditionList.java deleted file mode 100644 index 83f6debc5a..0000000000 --- a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/ConditionList.java +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.unomi.shell.commands; - -import org.apache.commons.lang3.StringUtils; -import org.apache.karaf.shell.api.action.Command; -import org.apache.karaf.shell.api.action.lifecycle.Reference; -import org.apache.karaf.shell.api.action.lifecycle.Service; -import org.apache.unomi.api.conditions.ConditionType; -import org.apache.unomi.api.services.DefinitionsService; -import org.apache.unomi.common.DataTable; - -import java.util.ArrayList; -import java.util.Collection; - -@Command(scope = "unomi", name = "condition-list", description = "This will list all the conditions deployed in the Apache Unomi Context Server") -@Service -public class ConditionList extends ListCommandSupport { - - @Reference - DefinitionsService definitionsService; - - @Override - protected String[] getHeaders() { - return new String[] { - "Id", - "Name", - "System tags" - }; - } - - @Override - protected DataTable buildDataTable() { - Collection allConditionTypes = definitionsService.getAllConditionTypes(); - - DataTable dataTable = new DataTable(); - - for (ConditionType conditionType : allConditionTypes) { - ArrayList rowData = new ArrayList<>(); - rowData.add(conditionType.getItemId()); - rowData.add(conditionType.getMetadata().getName()); - rowData.add(StringUtils.join(conditionType.getMetadata().getSystemTags(), ",")); - dataTable.addRow(rowData.toArray(new Comparable[rowData.size()])); - } - - dataTable.sort(new DataTable.SortCriteria(1, DataTable.SortOrder.ASCENDING)); - return dataTable; - } - -} diff --git a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/ConditionView.java b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/ConditionView.java deleted file mode 100644 index 03efe9d48a..0000000000 --- a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/ConditionView.java +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.unomi.shell.commands; - -import org.apache.karaf.shell.api.action.Action; -import org.apache.karaf.shell.api.action.Argument; -import org.apache.karaf.shell.api.action.Command; -import org.apache.karaf.shell.api.action.lifecycle.Reference; -import org.apache.karaf.shell.api.action.lifecycle.Service; -import org.apache.unomi.api.conditions.ConditionType; -import org.apache.unomi.api.services.DefinitionsService; -import org.apache.unomi.persistence.spi.CustomObjectMapper; - -@Command(scope = "unomi", name = "condition-view", description = "This will display a single condition deployed in the Apache Unomi Context Server") -@Service -public class ConditionView implements Action { - - @Reference - DefinitionsService definitionsService; - - @Argument(index = 0, name = "conditionId", description = "The identifier for the condition", required = true, multiValued = false) - String conditionTypeIdentifier; - - public Object execute() throws Exception { - ConditionType conditionType = definitionsService.getConditionType(conditionTypeIdentifier); - if (conditionType == null) { - System.out.println("Couldn't find an action with id=" + conditionTypeIdentifier); - return null; - } - String jsonRule = CustomObjectMapper.getObjectMapper().writerWithDefaultPrettyPrinter().writeValueAsString(conditionType); - System.out.println(jsonRule); - return null; - } - - -} diff --git a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/DeployDefinition.java b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/DeployDefinition.java deleted file mode 100644 index 9365de8a98..0000000000 --- a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/DeployDefinition.java +++ /dev/null @@ -1,113 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.unomi.shell.commands; - -import org.apache.karaf.shell.api.action.Command; -import org.apache.karaf.shell.api.action.lifecycle.Service; -import org.apache.unomi.api.Patch; -import org.apache.unomi.api.PersonaWithSessions; -import org.apache.unomi.api.PropertyType; -import org.apache.unomi.api.actions.ActionType; -import org.apache.unomi.api.campaigns.Campaign; -import org.apache.unomi.api.conditions.ConditionType; -import org.apache.unomi.api.goals.Goal; -import org.apache.unomi.api.rules.Rule; -import org.apache.unomi.api.segments.Scoring; -import org.apache.unomi.api.segments.Segment; -import org.apache.unomi.persistence.spi.CustomObjectMapper; - -import java.io.IOException; -import java.net.URL; - -@Command(scope = "unomi", name = "deploy-definition", description = "This will deploy Unomi definitions contained in bundles") -@Service -public class DeployDefinition extends DeploymentCommandSupport { - - public void processDefinition(String definitionType, URL definitionURL) { - try { - if (ALL_OPTION_LABEL.equals(definitionType)) { - String definitionURLString = definitionURL.toString(); - for (String possibleDefinitionType : definitionTypes) { - if (definitionURLString.contains(getDefinitionTypePath(possibleDefinitionType))) { - definitionType = possibleDefinitionType; - break; - } - } - if (ALL_OPTION_LABEL.equals(definitionType)) { - System.out.println("Couldn't resolve definition type for definition URL " + definitionURL); - return; - } - } - boolean successful = true; - switch (definitionType) { - case CONDITION_DEFINITION_TYPE: - ConditionType conditionType = CustomObjectMapper.getObjectMapper().readValue(definitionURL, ConditionType.class); - definitionsService.setConditionType(conditionType); - break; - case ACTION_DEFINITION_TYPE: - ActionType actionType = CustomObjectMapper.getObjectMapper().readValue(definitionURL, ActionType.class); - definitionsService.setActionType(actionType); - break; - case GOAL_DEFINITION_TYPE: - Goal goal = CustomObjectMapper.getObjectMapper().readValue(definitionURL, Goal.class); - goalsService.setGoal(goal); - break; - case CAMPAIGN_DEFINITION_TYPE: - Campaign campaign = CustomObjectMapper.getObjectMapper().readValue(definitionURL, Campaign.class); - goalsService.setCampaign(campaign); - break; - case PERSONA_DEFINITION_TYPE: - PersonaWithSessions persona = CustomObjectMapper.getObjectMapper().readValue(definitionURL, PersonaWithSessions.class); - profileService.savePersonaWithSessions(persona); - break; - case PROPERTY_DEFINITION_TYPE: - PropertyType propertyType = CustomObjectMapper.getObjectMapper().readValue(definitionURL, PropertyType.class); - profileService.setPropertyTypeTarget(definitionURL, propertyType); - profileService.setPropertyType(propertyType); - break; - case RULE_DEFINITION_TYPE: - Rule rule = CustomObjectMapper.getObjectMapper().readValue(definitionURL, Rule.class); - rulesService.setRule(rule); - break; - case SEGMENT_DEFINITION_TYPE: - Segment segment = CustomObjectMapper.getObjectMapper().readValue(definitionURL, Segment.class); - segmentService.setSegmentDefinition(segment); - break; - case SCORING_DEFINITION_TYPE: - Scoring scoring = CustomObjectMapper.getObjectMapper().readValue(definitionURL, Scoring.class); - segmentService.setScoringDefinition(scoring); - break; - case PATCH_DEFINITION_TYPE: - Patch patch = CustomObjectMapper.getObjectMapper().readValue(definitionURL, Patch.class); - patchService.patch(patch); - break; - default: - System.out.println("Unrecognized definition type:" + definitionType); - successful = false; - break; - } - if (successful) { - System.out.println("Predefined definition registered : " + definitionURL.getFile()); - } - } catch (IOException e) { - System.out.println("Error while saving definition " + definitionURL); - System.out.println(e.getMessage()); - } - } - - -} diff --git a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/EventList.java b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/EventList.java deleted file mode 100644 index b996a7085a..0000000000 --- a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/EventList.java +++ /dev/null @@ -1,90 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.unomi.shell.commands; - -import org.apache.karaf.shell.api.action.Argument; -import org.apache.karaf.shell.api.action.Command; -import org.apache.karaf.shell.api.action.lifecycle.Reference; -import org.apache.karaf.shell.api.action.lifecycle.Service; -import org.apache.unomi.api.Event; -import org.apache.unomi.api.PartialList; -import org.apache.unomi.api.conditions.Condition; -import org.apache.unomi.api.query.Query; -import org.apache.unomi.api.services.DefinitionsService; -import org.apache.unomi.api.services.EventService; -import org.apache.unomi.common.DataTable; - -import java.util.ArrayList; - -@Command(scope = "unomi", name = "event-list", description = "This commands lists the latest events updated in the Apache Unomi Context Server") -@Service -public class EventList extends ListCommandSupport { - - @Reference - private EventService eventService; - - @Reference - DefinitionsService definitionsService; - - @Argument(index = 0, name = "maxEntries", description = "The maximum number of entries to retrieve (defaults to 100)", required = false, multiValued = false) - int maxEntries = 100; - - @Argument(index = 1, name = "eventType", description = "If specified, will filter the event list by the given event type", required = false, multiValued = false) - String eventType = null; - - String[] columnHeaders = new String[] { - "ID", - "Type", - "Session", - "Profile", - "Timestamp", - "Scope", - "Persistent" - }; - - @Override - protected String[] getHeaders() { - return columnHeaders; - } - - @Override - protected DataTable buildDataTable() { - Condition condition = new Condition(definitionsService.getConditionType("matchAllCondition")); - if (eventType != null) { - condition = new Condition(definitionsService.getConditionType("eventTypeCondition")); - condition.setParameter("eventTypeId", eventType); - } - Query query = new Query(); - query.setLimit(maxEntries); - query.setCondition(condition); - query.setSortby("timeStamp:desc"); - PartialList lastEvents = eventService.search(query); - DataTable dataTable = new DataTable(); - for (Event event : lastEvents.getList()) { - ArrayList rowData = new ArrayList<>(); - rowData.add(event.getItemId()); - rowData.add(event.getEventType()); - rowData.add(event.getSessionId()); - rowData.add(event.getProfileId()); - rowData.add(event.getTimeStamp().toString()); - rowData.add(event.getScope()); - rowData.add(Boolean.toString(event.isPersistent())); - dataTable.addRow(rowData.toArray(new Comparable[rowData.size()])); - } - return dataTable; - } -} diff --git a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/EventSearch.java b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/EventSearch.java deleted file mode 100644 index 082f1d3d30..0000000000 --- a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/EventSearch.java +++ /dev/null @@ -1,99 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.unomi.shell.commands; - -import org.apache.karaf.shell.api.action.Argument; -import org.apache.karaf.shell.api.action.Command; -import org.apache.karaf.shell.api.action.lifecycle.Reference; -import org.apache.karaf.shell.api.action.lifecycle.Service; -import org.apache.unomi.api.Event; -import org.apache.unomi.api.PartialList; -import org.apache.unomi.api.conditions.Condition; -import org.apache.unomi.api.services.DefinitionsService; -import org.apache.unomi.api.services.EventService; -import org.apache.unomi.common.DataTable; - -import java.util.ArrayList; -import java.util.List; - -@Command(scope = "unomi", name = "event-search", description = "This commands search for profile events of a certain type by last timestamp in the Apache Unomi Context Server") -@Service -public class EventSearch extends ListCommandSupport { - @Reference - private EventService eventService; - - @Reference - DefinitionsService definitionsService; - - @Argument(index = 0, name = "profile", description = "The identifier for the profile", required = true, multiValued = false) - String profileIdentifier; - - @Argument(index = 1, name = "eventType", description = "The type of the event", required = false, multiValued = false) - String eventTypeId; - - @Argument(index = 2, name = "maxEntries", description = "The maximum number of entries to retrieve (defaults to 100)", required = false, multiValued = false) - int maxEntries = 100; - - String[] columnHeaders = new String[] { - "ID", - "Type", - "Session", - "Profile", - "Timestamp", - "Scope", - "Persistent" - }; - - @Override - protected String[] getHeaders() { - return columnHeaders; - } - - @Override - protected DataTable buildDataTable() { - Condition booleanCondition = new Condition(definitionsService.getConditionType("booleanCondition")); - booleanCondition.setParameter("operator", "and"); - List subConditions = new ArrayList<>(); - if (profileIdentifier != null) { - Condition eventProfileIdCondition = new Condition(definitionsService.getConditionType("eventPropertyCondition")); - eventProfileIdCondition.setParameter("propertyName", "profileId"); - eventProfileIdCondition.setParameter("comparisonOperator", "equals"); - eventProfileIdCondition.setParameter("propertyValue", profileIdentifier); - subConditions.add(eventProfileIdCondition); - } - if (eventTypeId != null) { - Condition eventTypeIdCondition = new Condition(definitionsService.getConditionType("eventTypeCondition")); - eventTypeIdCondition.setParameter("eventTypeId", eventTypeId); - subConditions.add(eventTypeIdCondition); - } - booleanCondition.setParameter("subConditions", subConditions); - PartialList lastEvents = eventService.searchEvents(booleanCondition, 0, maxEntries); - DataTable dataTable = new DataTable(); - for (Event event : lastEvents.getList()) { - ArrayList rowData = new ArrayList<>(); - rowData.add(event.getItemId()); - rowData.add(event.getEventType()); - rowData.add(event.getSessionId()); - rowData.add(event.getProfileId()); - rowData.add(event.getTimeStamp().toString()); - rowData.add(event.getScope()); - rowData.add(Boolean.toString(event.isPersistent())); - dataTable.addRow(rowData.toArray(new Comparable[rowData.size()])); - } - return dataTable; - } -} diff --git a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/EventView.java b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/EventView.java deleted file mode 100644 index 10054ddd46..0000000000 --- a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/EventView.java +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.unomi.shell.commands; - -import org.apache.karaf.shell.api.action.Action; -import org.apache.karaf.shell.api.action.Argument; -import org.apache.karaf.shell.api.action.Command; -import org.apache.karaf.shell.api.action.lifecycle.Reference; -import org.apache.karaf.shell.api.action.lifecycle.Service; -import org.apache.unomi.api.Event; -import org.apache.unomi.api.PartialList; -import org.apache.unomi.api.conditions.Condition; -import org.apache.unomi.api.services.DefinitionsService; -import org.apache.unomi.api.services.EventService; -import org.apache.unomi.persistence.spi.CustomObjectMapper; - -@Command(scope = "unomi", name = "event-view", description = "This command will dump an Event as a JSON object") -@Service -public class EventView implements Action { - - @Reference - EventService eventService; - - @Reference - DefinitionsService definitionsService; - - @Argument(index = 0, name = "event", description = "The identifier for the event", required = true, multiValued = false) - String eventIdentifier; - - public Object execute() throws Exception { - - Condition eventCondition = new Condition(definitionsService.getConditionType("eventPropertyCondition")); - eventCondition.setParameter("propertyName", "itemId"); - eventCondition.setParameter("comparisonOperator", "equals"); - eventCondition.setParameter("propertyValue", eventIdentifier); - - PartialList matchingEvents = eventService.searchEvents(eventCondition, 0, 10); - if (matchingEvents == null || matchingEvents.getTotalSize() != 1) { - System.out.println("Couldn't find a single event with id=" + eventIdentifier + ". Maybe it wasn't a persistent event ?"); - return null; - } - String jsonEvent = CustomObjectMapper.getObjectMapper().writerWithDefaultPrettyPrinter().writeValueAsString(matchingEvents.get(0)); - System.out.println(jsonEvent); - return null; - } -} diff --git a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/ProfileList.java b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/ProfileList.java deleted file mode 100644 index 24f3a2c97b..0000000000 --- a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/ProfileList.java +++ /dev/null @@ -1,84 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.unomi.shell.commands; - -import org.apache.commons.lang3.StringUtils; -import org.apache.karaf.shell.api.action.Argument; -import org.apache.karaf.shell.api.action.Command; -import org.apache.karaf.shell.api.action.lifecycle.Reference; -import org.apache.karaf.shell.api.action.lifecycle.Service; -import org.apache.unomi.api.PartialList; -import org.apache.unomi.api.Profile; -import org.apache.unomi.api.conditions.Condition; -import org.apache.unomi.api.query.Query; -import org.apache.unomi.api.services.DefinitionsService; -import org.apache.unomi.api.services.ProfileService; -import org.apache.unomi.common.DataTable; - -import java.util.ArrayList; - -@Command(scope = "unomi", name = "profile-list", description = "This commands lists the latest profiles updated in the Apache Unomi Context Server") -@Service -public class ProfileList extends ListCommandSupport { - - @Reference - ProfileService profileService; - - @Reference - DefinitionsService definitionsService; - - @Argument(index = 0, name = "maxEntries", description = "The maximum number of entries to retrieve (defaults to 100)", required = false, multiValued = false) - int maxEntries = 100; - - @java.lang.Override - protected String[] getHeaders() { - return new String[] { - "ID", - "Scope", - "Segments", - "Consents", - "Last visit", - "Last update" - }; - } - - @java.lang.Override - protected DataTable buildDataTable() { - Query query = new Query(); - query.setSortby("systemProperties.lastUpdated:desc,properties.lastVisit:desc"); - query.setLimit(maxEntries); - Condition matchAllCondition = new Condition(definitionsService.getConditionType("matchAllCondition")); - query.setCondition(matchAllCondition); - PartialList lastModifiedProfiles = profileService.search(query, Profile.class); - DataTable dataTable = new DataTable(); - for (Profile profile : lastModifiedProfiles.getList()) { - ArrayList rowData = new ArrayList<>(); - rowData.add(profile.getItemId()); - rowData.add(profile.getScope()); - rowData.add(StringUtils.join(profile.getSegments(), ",")); - rowData.add(StringUtils.join(profile.getConsents().keySet(), ",")); - rowData.add((String) profile.getProperty("lastVisit")); - if (profile.getSystemProperties() != null && profile.getSystemProperties().get("lastUpdated") != null) { - rowData.add((String) profile.getSystemProperties().get("lastUpdated")); - } else { - rowData.add(""); - } - dataTable.addRow(rowData.toArray(new Comparable[rowData.size()])); - } - return dataTable; - } -} diff --git a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/ProfileView.java b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/ProfileView.java deleted file mode 100644 index 89705790f1..0000000000 --- a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/ProfileView.java +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.unomi.shell.commands; - -import org.apache.karaf.shell.api.action.Action; -import org.apache.karaf.shell.api.action.Argument; -import org.apache.karaf.shell.api.action.Command; -import org.apache.karaf.shell.api.action.lifecycle.Reference; -import org.apache.karaf.shell.api.action.lifecycle.Service; -import org.apache.unomi.api.Profile; -import org.apache.unomi.api.services.ProfileService; -import org.apache.unomi.persistence.spi.CustomObjectMapper; - -@Command(scope = "unomi", name = "profile-view", description = "This command will dump a profile as a JSON string") -@Service -public class ProfileView implements Action { - - @Reference - ProfileService profileService; - - @Argument(index = 0, name = "profile", description = "The identifier for the profile", required = true, multiValued = false) - String profileIdentifier; - - public Object execute() throws Exception { - Profile profile = profileService.load(profileIdentifier); - if (profile == null) { - System.out.println("Couldn't find a profile with id=" + profileIdentifier); - return null; - } - String jsonProfile = CustomObjectMapper.getObjectMapper().writerWithDefaultPrettyPrinter().writeValueAsString(profile); - System.out.println(jsonProfile); - return null; - } -} diff --git a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/RuleList.java b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/RuleList.java deleted file mode 100644 index de5a4d8c62..0000000000 --- a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/RuleList.java +++ /dev/null @@ -1,108 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.unomi.shell.commands; - -import org.apache.commons.lang3.StringUtils; -import org.apache.karaf.shell.api.action.Argument; -import org.apache.karaf.shell.api.action.Command; -import org.apache.karaf.shell.api.action.lifecycle.Reference; -import org.apache.karaf.shell.api.action.lifecycle.Service; -import org.apache.unomi.api.Metadata; -import org.apache.unomi.api.PartialList; -import org.apache.unomi.api.conditions.Condition; -import org.apache.unomi.api.query.Query; -import org.apache.unomi.api.rules.RuleStatistics; -import org.apache.unomi.api.services.DefinitionsService; -import org.apache.unomi.api.services.RulesService; -import org.apache.unomi.common.DataTable; - -import java.util.ArrayList; -import java.util.Map; - -@Command(scope = "unomi", name = "rule-list", description = "This will list all the rules deployed in the Apache Unomi Context Server") -@Service -public class RuleList extends ListCommandSupport { - - @Reference - RulesService rulesService; - - @Reference - DefinitionsService definitionsService; - - @Argument(index = 0, name = "maxEntries", description = "The maximum number of entries to retrieve (defaults to 100)", required = false, multiValued = false) - int maxEntries = 100; - - @Override - protected String[] getHeaders() { - return new String[] { - "Activated", - "Hidden", - "Read-only", - "Identifier", - "Scope", - "Name", - "Tags", - "System tags", - "Executions", - "Conditions [ms]", - "Actions [ms]" - }; - } - - @Override - protected DataTable buildDataTable() { - Query query = new Query(); - Condition matchAllCondition = new Condition(definitionsService.getConditionType("matchAllCondition")); - query.setCondition(matchAllCondition); - query.setLimit(maxEntries); - PartialList ruleMetadatas = rulesService.getRuleMetadatas(query); - if (ruleMetadatas.getList().size() != ruleMetadatas.getTotalSize()) { - System.out.println("WARNING : Only the first " + ruleMetadatas.getPageSize() + " rules have been retrieved, there are " + ruleMetadatas.getTotalSize() + " rules registered in total. Use the maxEntries parameter to retrieve more rules"); - } - Map allRuleStatistics = rulesService.getAllRuleStatistics(); - - DataTable dataTable = new DataTable(); - for (Metadata ruleMetadata : ruleMetadatas.getList()) { - ArrayList rowData = new ArrayList<>(); - String ruleId = ruleMetadata.getId(); - rowData.add(ruleMetadata.isEnabled() ? "x" : ""); - rowData.add(ruleMetadata.isHidden() ? "x" : ""); - rowData.add(ruleMetadata.isReadOnly() ? "x" : ""); - rowData.add(ruleId); - rowData.add(ruleMetadata.getScope()); - rowData.add(ruleMetadata.getName()); - rowData.add(StringUtils.join(ruleMetadata.getTags(), ",")); - rowData.add(StringUtils.join(ruleMetadata.getSystemTags(), ",")); - RuleStatistics ruleStatistics = allRuleStatistics.get(ruleId); - if (ruleStatistics != null) { - rowData.add(ruleStatistics.getExecutionCount()); - rowData.add(ruleStatistics.getConditionsTime()); - rowData.add(ruleStatistics.getActionsTime()); - } else { - rowData.add(0L); - rowData.add(0L); - rowData.add(0L); - } - dataTable.addRow(rowData.toArray(new Comparable[rowData.size()])); - } - dataTable.sort(new DataTable.SortCriteria(9, DataTable.SortOrder.DESCENDING), - new DataTable.SortCriteria(10, DataTable.SortOrder.DESCENDING), - new DataTable.SortCriteria(5, DataTable.SortOrder.ASCENDING)); - return dataTable; - } - -} diff --git a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/SegmentList.java b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/SegmentList.java deleted file mode 100644 index 5f8395137a..0000000000 --- a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/SegmentList.java +++ /dev/null @@ -1,73 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.unomi.shell.commands; - -import org.apache.commons.lang3.StringUtils; -import org.apache.karaf.shell.api.action.Argument; -import org.apache.karaf.shell.api.action.Command; -import org.apache.karaf.shell.api.action.lifecycle.Reference; -import org.apache.karaf.shell.api.action.lifecycle.Service; -import org.apache.unomi.api.Metadata; -import org.apache.unomi.api.PartialList; -import org.apache.unomi.api.services.SegmentService; -import org.apache.unomi.common.DataTable; - -import java.util.ArrayList; - -@Command(scope = "unomi", name = "segment-list", description = "This will list all the segments present in the Apache Unomi Context Server") -@Service -public class SegmentList extends ListCommandSupport { - - @Reference - SegmentService segmentService; - - @Argument(index = 0, name = "maxEntries", description = "The maximum number of entries to retrieve (defaults to 100)", required = false, multiValued = false) - int maxEntries = 100; - - @Override - protected String[] getHeaders() { - return new String[] { - "Enabled", - "Hidden", - "Id", - "Scope", - "Name", - "System tags" - }; - } - - @Override - protected DataTable buildDataTable() { - PartialList segmentMetadatas = segmentService.getSegmentMetadatas(0, maxEntries, null); - - DataTable dataTable = new DataTable(); - for (Metadata metadata : segmentMetadatas.getList()) { - ArrayList rowData = new ArrayList<>(); - rowData.add(metadata.isEnabled() ? "x" : ""); - rowData.add(metadata.isHidden() ? "x" : ""); - rowData.add(metadata.getId()); - rowData.add(metadata.getScope()); - rowData.add(metadata.getName()); - rowData.add(StringUtils.join(metadata.getSystemTags(), ",")); - dataTable.addRow(rowData.toArray(new Comparable[rowData.size()])); - } - - dataTable.sort(new DataTable.SortCriteria(4, DataTable.SortOrder.ASCENDING)); - return dataTable; - } - -} diff --git a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/SegmentRemove.java b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/SegmentRemove.java deleted file mode 100644 index 14b4efdf1c..0000000000 --- a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/SegmentRemove.java +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.unomi.shell.commands; - -import org.apache.karaf.shell.api.action.Action; -import org.apache.karaf.shell.api.action.Argument; -import org.apache.karaf.shell.api.action.Command; -import org.apache.karaf.shell.api.action.lifecycle.Reference; -import org.apache.karaf.shell.api.action.lifecycle.Service; -import org.apache.unomi.api.segments.DependentMetadata; -import org.apache.unomi.api.services.SegmentService; - -@Command(scope = "unomi", name = "segment-remove", description = "Remove segments in the Apache Unomi Context Server") -@Service -public class SegmentRemove implements Action { - - @Reference - SegmentService segmentService; - - @Argument(index = 0, name = "segmentId", description = "The identifier for the segment", required = true, multiValued = false) - String segmentIdentifier; - - @Argument(index = 1, name = "validate", description = "Check if the segment is used in goals or other segments", required = false, multiValued = false) - Boolean validate = true; - - - public Object execute() throws Exception { - DependentMetadata dependantMetadata = segmentService.removeSegmentDefinition(segmentIdentifier, validate); - if (!validate || (dependantMetadata.getSegments().isEmpty() && dependantMetadata.getScorings().isEmpty())) { - System.out.println("Segment " + segmentIdentifier + " successfully deleted"); - } else if (validate) { - System.out.print("Segment " + segmentIdentifier + " could not be deleted because of the following dependents:"); - if (!dependantMetadata.getScorings().isEmpty()) { - System.out.print(" scoring:" + dependantMetadata.getScorings()); - } - if (!dependantMetadata.getSegments().isEmpty()) { - System.out.println(" segments:" + dependantMetadata.getSegments()); - } - } - return null; - } -} diff --git a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/SegmentView.java b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/SegmentView.java deleted file mode 100644 index 9016aa9ed0..0000000000 --- a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/SegmentView.java +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.unomi.shell.commands; - -import org.apache.karaf.shell.api.action.Action; -import org.apache.karaf.shell.api.action.Argument; -import org.apache.karaf.shell.api.action.Command; -import org.apache.karaf.shell.api.action.lifecycle.Reference; -import org.apache.karaf.shell.api.action.lifecycle.Service; -import org.apache.unomi.api.segments.Segment; -import org.apache.unomi.api.services.SegmentService; -import org.apache.unomi.persistence.spi.CustomObjectMapper; - -@Command(scope = "unomi", name = "segment-view", description = "This will allows to view a segment in the Apache Unomi Context Server") -@Service -public class SegmentView implements Action { - - @Reference - SegmentService segmentService; - - @Argument(index = 0, name = "segmentId", description = "The identifier for the segment", required = true, multiValued = false) - String segmentIdentifier; - - public Object execute() throws Exception { - Segment segment = segmentService.getSegmentDefinition(segmentIdentifier); - if (segment == null) { - System.out.println("Couldn't find a segment with id=" + segmentIdentifier); - return null; - } - String jsonRule = CustomObjectMapper.getObjectMapper().writerWithDefaultPrettyPrinter().writeValueAsString(segment); - System.out.println(jsonRule); - return null; - } -} diff --git a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/SessionList.java b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/SessionList.java deleted file mode 100644 index a3d0fda989..0000000000 --- a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/SessionList.java +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.unomi.shell.commands; - -import org.apache.karaf.shell.api.action.Argument; -import org.apache.karaf.shell.api.action.Command; -import org.apache.karaf.shell.api.action.lifecycle.Reference; -import org.apache.karaf.shell.api.action.lifecycle.Service; -import org.apache.unomi.api.PartialList; -import org.apache.unomi.api.Session; -import org.apache.unomi.api.conditions.Condition; -import org.apache.unomi.api.query.Query; -import org.apache.unomi.api.services.DefinitionsService; -import org.apache.unomi.api.services.ProfileService; -import org.apache.unomi.common.DataTable; - -import java.util.ArrayList; - -@Command(scope = "unomi", name = "session-list", description = "This commands lists the latest sessions updated in the Apache Unomi Context Server") -@Service -public class SessionList extends ListCommandSupport { - - @Reference - ProfileService profileService; - - @Reference - DefinitionsService definitionsService; - - @Argument(index = 0, name = "maxEntries", description = "The maximum number of entries to retrieve (defaults to 100)", required = false, multiValued = false) - int maxEntries = 100; - - @java.lang.Override - protected String[] getHeaders() { - return new String[] { - "ID", - "Scope", - "Last event", - "Duration", - "Profile", - "Timestamp" - }; - } - - @java.lang.Override - protected DataTable buildDataTable() { - Query query = new Query(); - query.setSortby("lastEventDate:desc"); - query.setLimit(maxEntries); - Condition matchAllCondition = new Condition(definitionsService.getConditionType("matchAllCondition")); - query.setCondition(matchAllCondition); - PartialList lastModifiedProfiles = profileService.searchSessions(query); - DataTable dataTable = new DataTable(); - for (Session session : lastModifiedProfiles.getList()) { - ArrayList rowData = new ArrayList<>(); - rowData.add(session.getItemId()); - rowData.add(session.getScope()); - rowData.add(session.getLastEventDate()); - rowData.add(session.getDuration()); - rowData.add(session.getProfileId()); - rowData.add(session.getTimeStamp()); - dataTable.addRow(rowData.toArray(new Comparable[rowData.size()])); - } - return dataTable; - } -} diff --git a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/UndeployDefinition.java b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/UndeployDefinition.java deleted file mode 100644 index e672bff149..0000000000 --- a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/UndeployDefinition.java +++ /dev/null @@ -1,111 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.unomi.shell.commands; - -import org.apache.karaf.shell.api.action.Command; -import org.apache.karaf.shell.api.action.lifecycle.Service; -import org.apache.unomi.api.Patch; -import org.apache.unomi.api.PersonaWithSessions; -import org.apache.unomi.api.PropertyType; -import org.apache.unomi.api.actions.ActionType; -import org.apache.unomi.api.campaigns.Campaign; -import org.apache.unomi.api.conditions.ConditionType; -import org.apache.unomi.api.goals.Goal; -import org.apache.unomi.api.rules.Rule; -import org.apache.unomi.api.segments.Scoring; -import org.apache.unomi.api.segments.Segment; -import org.apache.unomi.persistence.spi.CustomObjectMapper; - -import java.io.IOException; -import java.net.URL; - -@Command(scope = "unomi", name = "undeploy-definition", description = "This will undeploy definitions contained in bundles") -@Service -public class UndeployDefinition extends DeploymentCommandSupport { - - public void processDefinition(String definitionType, URL definitionURL) { - try { - if (ALL_OPTION_LABEL.equals(definitionType)) { - String definitionURLString = definitionURL.toString(); - for (String possibleDefinitionType : definitionTypes) { - if (definitionURLString.contains(getDefinitionTypePath(possibleDefinitionType))) { - definitionType = possibleDefinitionType; - break; - } - } - if (ALL_OPTION_LABEL.equals(definitionType)) { - System.out.println("Couldn't resolve definition type for definition URL " + definitionURL); - return; - } - } - boolean successful = true; - switch (definitionType) { - case CONDITION_DEFINITION_TYPE: - ConditionType conditionType = CustomObjectMapper.getObjectMapper().readValue(definitionURL, ConditionType.class); - definitionsService.removeActionType(conditionType.getItemId()); - break; - case ACTION_DEFINITION_TYPE: - ActionType actionType = CustomObjectMapper.getObjectMapper().readValue(definitionURL, ActionType.class); - definitionsService.removeActionType(actionType.getItemId()); - break; - case GOAL_DEFINITION_TYPE: - Goal goal = CustomObjectMapper.getObjectMapper().readValue(definitionURL, Goal.class); - goalsService.removeGoal(goal.getItemId()); - break; - case CAMPAIGN_DEFINITION_TYPE: - Campaign campaign = CustomObjectMapper.getObjectMapper().readValue(definitionURL, Campaign.class); - goalsService.removeCampaign(campaign.getItemId()); - break; - case PERSONA_DEFINITION_TYPE: - PersonaWithSessions persona = CustomObjectMapper.getObjectMapper().readValue(definitionURL, PersonaWithSessions.class); - profileService.delete(persona.getPersona().getItemId(), true); - break; - case PROPERTY_DEFINITION_TYPE: - PropertyType propertyType = CustomObjectMapper.getObjectMapper().readValue(definitionURL, PropertyType.class); - profileService.deletePropertyType(propertyType.getItemId()); - break; - case RULE_DEFINITION_TYPE: - Rule rule = CustomObjectMapper.getObjectMapper().readValue(definitionURL, Rule.class); - rulesService.removeRule(rule.getItemId()); - break; - case SEGMENT_DEFINITION_TYPE: - Segment segment = CustomObjectMapper.getObjectMapper().readValue(definitionURL, Segment.class); - segmentService.removeSegmentDefinition(segment.getItemId(), false); - break; - case SCORING_DEFINITION_TYPE: - Scoring scoring = CustomObjectMapper.getObjectMapper().readValue(definitionURL, Scoring.class); - segmentService.removeScoringDefinition(scoring.getItemId(), false); - break; - case PATCH_DEFINITION_TYPE: - Patch patch = CustomObjectMapper.getObjectMapper().readValue(definitionURL, Patch.class); - // patchService.patch(patch); - break; - default: - System.out.println("Unrecognized definition type: " + definitionType); - successful = false; - break; - } - if (successful) { - System.out.println("Predefined definition unregistered : " + definitionURL.getFile()); - } - } catch (IOException e) { - System.out.println("Error while removing definition " + definitionURL); - System.out.println(e.getMessage()); - } - } - -} diff --git a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/actions/UnomiCrudCommand.java b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/actions/UnomiCrudCommand.java new file mode 100644 index 0000000000..2ca8433f15 --- /dev/null +++ b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/actions/UnomiCrudCommand.java @@ -0,0 +1,608 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.unomi.shell.dev.actions; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.commons.lang3.StringUtils; +import org.apache.unomi.persistence.spi.CustomObjectMapper; +import org.apache.karaf.shell.api.action.*; +import org.apache.karaf.shell.api.action.lifecycle.Init; +import org.apache.karaf.shell.api.action.lifecycle.Reference; +import org.apache.karaf.shell.api.action.lifecycle.Service; +import org.apache.karaf.shell.api.console.Session; +import org.apache.karaf.shell.support.table.ShellTable; + +import java.io.PrintStream; +import org.apache.unomi.shell.dev.completers.IdCompleter; +import org.apache.unomi.shell.dev.completers.OperationCompleter; +import org.apache.unomi.shell.dev.completers.TypeCompleter; +import org.apache.unomi.shell.dev.services.CrudCommand; +import org.osgi.framework.BundleContext; +import org.osgi.framework.ServiceReference; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.List; +import java.util.Map; + +@Command(scope = "unomi", name = "crud", description = "Perform CRUD operations on Unomi objects") +@Service +public class UnomiCrudCommand implements Action { + + private static final Logger LOGGER = LoggerFactory.getLogger(UnomiCrudCommand.class.getName()); + + private static final ObjectMapper OBJECT_MAPPER = CustomObjectMapper.getObjectMapper(); + + @Reference + private BundleContext bundleContext; + + @Reference + private Session session; + + @Argument(index = 0, name = "operation", description = "Operation to perform (create/read/update/delete/list/help)", required = true) + @Completion(OperationCompleter.class) + private String operation; + + @Argument(index = 1, name = "type", description = "Object type", required = true) + @Completion(TypeCompleter.class) + private String type; + + // Multi-valued argument that captures all remaining tokens after type + // ⚠️ IMPORTANT: Only the last argument (highest index) can be multi-valued in Karaf + // Since this is at index 2 (last argument), it can safely be multi-valued + // For create: remaining[0] = JSON/URL + // For read/delete: remaining[0] = ID + // For update: remaining[0] = ID, remaining[1] = JSON/URL + // For list: remaining contains all remaining tokens (--csv, -n, 50, etc.) for manual parsing + @Argument(index = 2, name = "remaining", description = "ID/JSON/URL (for create/read/update/delete) or remaining tokens (for list)", required = false, multiValued = true) + @Completion(IdCompleter.class) // Could be enhanced to detect context + private List remaining; + + // Option fields for list operation (populated via manual parsing from remaining) + @Option(name = "--csv", description = "Output list in CSV format", required = false, multiValued = false) + private boolean csv; + + @Option(name = "-n", aliases = "--max-entries", description = "Maximum number of entries to list", required = false) + private Integer maxEntries; + + @Init + public void init() { + LOGGER.debug("UnomiCrudCommand init"); + } + + /** + * Check if a token is a max-entries option flag. + * + * @param token the token to check + * @return true if the token is -n or --max-entries + */ + private boolean isMaxEntriesOption(String token) { + return "-n".equals(token) || "--max-entries".equals(token); + } + + /** + * Parse max-entries option value from the remaining list. + * Validates that the value is a positive integer. + * + * @param remaining the remaining argument list + * @param index the index of the option flag + * @return the parsed integer value (must be > 0), or null if invalid/missing + */ + private Integer parseMaxEntriesValue(List remaining, int index) { + if (index + 1 >= remaining.size()) { + return null; + } + try { + int value = Integer.parseInt(remaining.get(index + 1)); + // Only accept positive values + if (value <= 0) { + LOGGER.warn("Invalid max-entries value (must be positive): " + value); + return null; + } + return value; + } catch (NumberFormatException e) { + LOGGER.warn("Invalid number for max-entries option: " + remaining.get(index + 1)); + return null; + } + } + + /** + * Parse list-specific options from the remaining argument list. + * This implements Option 1: Simple Manual Parsing from the redesign proposal. + * + * Note: If --csv was already set by Karaf's option parser (when placed before arguments), + * we preserve that value. Otherwise, we parse it from the remaining list. + * + * @param remaining List of remaining tokens after type (e.g., ["--csv", "-n", "50"]) + */ + private void parseListOptions(List remaining) { + // Preserve csv value if already set by Karaf's option parser (when --csv comes before arguments) + boolean csv = this.csv; + Integer maxEntries = this.maxEntries; + + if (remaining == null || remaining.isEmpty()) { + // Keep existing values if already set by Karaf + return; + } + + for (int i = 0; i < remaining.size(); i++) { + String token = remaining.get(i); + + if ("--csv".equals(token)) { + csv = true; + } else if (isMaxEntriesOption(token)) { + Integer value = parseMaxEntriesValue(remaining, i); + if (value != null) { + maxEntries = value; + i++; // Skip the next token as it's the value + } + } + // Ignore unknown tokens (could log warning) + } + + // Populate option fields + this.csv = csv; + this.maxEntries = maxEntries; + } + + /** + * Check if remaining argument list has at least the specified number of non-empty elements. + * + * @param remaining the remaining argument list + * @param minSize minimum number of elements required + * @return true if valid, false otherwise + */ + private boolean hasMinimumRemainingArgs(List remaining, int minSize) { + if (remaining == null || remaining.size() < minSize) { + return false; + } + for (int i = 0; i < minSize; i++) { + if (StringUtils.isBlank(remaining.get(i))) { + return false; + } + } + return true; + } + + /** + * Parse JSON properties from remaining argument or URL with error handling. + * This method wraps parseProperties() and handles exceptions, providing consistent error messages. + * + * @param jsonOrUrl JSON string or URL from remaining argument + * @param console Console for error output + * @return Map of properties, or null if parsing failed or error occurred + */ + private Map parsePropertiesWithErrorHandling(String jsonOrUrl, PrintStream console) { + final String errorMsg = "Error: Failed to parse JSON or URL: " + jsonOrUrl; + try { + Map props = parseProperties(jsonOrUrl); + if (props == null) { + console.println(errorMsg); + } + return props; + } catch (Exception e) { + console.println(errorMsg); + console.println("Error details: " + e.getMessage()); + return null; + } + } + + /** + * Strip surrounding quotes from a string if present. + * + * @param str the string to process + * @return the string with quotes removed, or original if no quotes + */ + private String stripQuotes(String str) { + if (StringUtils.isEmpty(str) || str.length() < 2) { + return str; + } + // Check for single quotes + if (str.charAt(0) == '\'' && str.charAt(str.length() - 1) == '\'') { + return str.substring(1, str.length() - 1); + } + // Check for double quotes + if (str.charAt(0) == '"' && str.charAt(str.length() - 1) == '"') { + return str.substring(1, str.length() - 1); + } + return str; + } + + /** + * Check if a string is a valid URL by attempting to parse it as a URI. + * This method supports all URL schemes that Pax URL supports: + * - file:// (file protocol) + * - http://, https:// (HTTP/HTTPS protocols) + * - mvn: (Maven protocol) + * - war: (War protocol) + * - Any other valid URI scheme + * + * The method uses Java's URI class to validate the scheme, which is more + * robust than simple string matching and supports all standard and custom schemes. + * + * @param str the string to check + * @return true if the string is a valid URI with a scheme, false otherwise + */ + private boolean isUrl(String str) { + if (StringUtils.isBlank(str)) { + return false; + } + + // JSON strings typically start with { or [, so they're not URLs + String trimmed = str.trim(); + if (trimmed.startsWith("{") || trimmed.startsWith("[")) { + return false; + } + + try { + URI uri = new URI(trimmed); + // A valid URI with a scheme is considered a URL + // getScheme() returns null for relative URIs, which are not URLs + return uri.getScheme() != null; + } catch (URISyntaxException e) { + // Not a valid URI, so not a URL + return false; + } + } + + /** + * Parse JSON from a file URL. + * + * @param fileUrl the file:// URL + * @return the parsed JSON as a Map + * @throws Exception if there's an error reading or parsing the file + */ + private Map parseFileUrl(String fileUrl) throws Exception { + URI uri = new URI(fileUrl); + String scheme = uri.getScheme(); + + if (!"file".equals(scheme)) { + throw new IllegalArgumentException("Expected file:// URL, got: " + fileUrl); + } + + // Handle file:// URLs - getPath() handles both file:///path and file://path + String filePath = uri.getPath(); + if (filePath == null || filePath.isEmpty()) { + throw new IllegalArgumentException("Invalid file URL: " + fileUrl); + } + + @SuppressWarnings("unchecked") + Map result = OBJECT_MAPPER.readValue(Files.readString(Paths.get(filePath)), Map.class); + return result; + } + + /** + * Parse JSON properties from remaining argument or URL. + * Supports: + * - Inline JSON string: {"itemId":"test"} (quoted or unquoted) + * - File URL: file:///path/to/file.json + * - HTTP/HTTPS URL: http://example.com/data.json (not yet implemented) + * - Maven URL: mvn:groupId/artifactId/version (not yet implemented) + * - War URL: war:file://path/to.war (not yet implemented) + * - Any other Pax URL supported scheme (not yet implemented) + * + * Note: If JSON is quoted in the command (e.g., '{"itemId":"test"}'), + * the Gogo parser will strip the quotes before passing to this method. + * This method handles both quoted and unquoted JSON strings. + * + * @param jsonOrUrl JSON string or URL from remaining argument + * @return Map of properties, or null if invalid + * @throws Exception if there's an error parsing the JSON or reading the URL + */ + private Map parseProperties(String jsonOrUrl) throws Exception { + if (StringUtils.isBlank(jsonOrUrl)) { + return null; + } + + String trimmed = stripQuotes(StringUtils.trim(jsonOrUrl)); + + if (isUrl(trimmed)) { + URI uri = new URI(trimmed); + String scheme = uri.getScheme(); + + if ("file".equals(scheme)) { + return parseFileUrl(trimmed); + } else { + // Other URL schemes (http, https, mvn, war, etc.) are not yet supported + // In the future, we could use Pax URL's URLStreamHandler to resolve these + throw new UnsupportedOperationException( + "URL scheme '" + scheme + "' is not yet supported. " + + "Currently only file:// URLs are supported. Use file:// or inline JSON."); + } + } + + // Treat as inline JSON + @SuppressWarnings("unchecked") + Map result = OBJECT_MAPPER.readValue(trimmed, Map.class); + return result; + } + + /** + * Validate that operation and type are provided. + * + * @param console console for error output + * @return true if valid, false otherwise + */ + private boolean validateOperationAndType(PrintStream console) { + if (StringUtils.isBlank(operation)) { + console.println("Error: Operation is required"); + console.println("Usage: unomi:crud [remaining...]"); + console.println("Available operations: create, read, update, delete, list, help"); + return false; + } + + if (StringUtils.isBlank(type)) { + console.println("Error: Type is required"); + console.println("Usage: unomi:crud [remaining...]"); + return false; + } + + return true; + } + + /** + * Execute the appropriate handler for the given operation. + * + * @param cmd the CrudCommand instance + * @param operationLower the lowercase operation name + * @param console console for output + * @return the result of the operation + * @throws Exception if the operation fails + */ + private Object executeOperation(CrudCommand cmd, String operationLower, PrintStream console) throws Exception { + switch (operationLower) { + case "create": + return handleCreate(cmd, console); + + case "read": + return handleRead(cmd, console); + + case "update": + return handleUpdate(cmd, console); + + case "delete": + return handleDelete(cmd, console); + + case "list": + return handleList(cmd, console); + + case "help": + console.println("Properties for " + type + ":"); + console.println(cmd.getPropertiesHelp()); + return null; + + default: + console.println("Unknown operation: " + operation); + console.println("Available operations: create, read, update, delete, list, help"); + return null; + } + } + + /** + * Find and execute the CrudCommand for the given type. + * + * @param console console for output + * @return true if a handler was found and executed, false otherwise + * @throws Exception if the operation fails + */ + private boolean findAndExecuteCommand(PrintStream console) throws Exception { + ServiceReference[] refs = bundleContext.getAllServiceReferences(CrudCommand.class.getName(), null); + if (refs == null) { + return false; + } + + String operationLower = operation.toLowerCase(); + for (ServiceReference ref : refs) { + CrudCommand cmd = (CrudCommand) bundleContext.getService(ref); + if (cmd.getObjectType().equals(type)) { + try { + executeOperation(cmd, operationLower, console); + return true; // Handler found and executed + } finally { + bundleContext.ungetService(ref); + } + } + } + return false; // No handler found + } + + @Override + public Object execute() throws Exception { + PrintStream console = session.getConsole(); + + if (!validateOperationAndType(console)) { + return null; + } + + boolean handlerFound = findAndExecuteCommand(console); + if (!handlerFound) { + console.println("No handler found for object type: " + type); + } + return null; + } + + /** + * Validate that remaining argument list has at least one non-empty element for create operation. + * + * @param remaining the remaining argument list + * @param console console for error output + * @return true if valid, false otherwise + */ + private boolean validateCreateRemaining(List remaining, PrintStream console) { + if (!hasMinimumRemainingArgs(remaining, 1)) { + console.println("Error: JSON string or URL is required for create operation"); + console.println("Usage: unomi:crud create "); + console.println("Example: unomi:crud create goal '{\"itemId\":\"test\",\"enabled\":true}'"); + console.println("Example: unomi:crud create goal file:///path/to/file.json"); + console.println("Note: Quote JSON strings to ensure they're treated as a single argument"); + return false; + } + return true; + } + + /** + * Handle create operation. + * Syntax: unomi:crud create + * remaining[0] = JSON string or URL + */ + private Object handleCreate(CrudCommand cmd, PrintStream console) throws Exception { + if (!validateCreateRemaining(remaining, console)) { + return null; + } + + String jsonOrUrl = remaining.get(0); + Map createProps = parsePropertiesWithErrorHandling(jsonOrUrl, console); + if (createProps == null) { + return null; + } + + // Validate that we have at least some properties (empty JSON {} is not valid) + if (createProps.isEmpty()) { + console.println("Error: Empty JSON object is not valid. Please provide required properties."); + console.println("Usage: unomi:crud create "); + return null; + } + + String newId = cmd.create(createProps); + if (newId == null) { + console.println("Error: Failed to create " + type + ". The create operation returned null."); + return null; + } + console.println("Created " + type + " with ID: " + newId); + return null; + } + + /** + * Validate that remaining argument list has at least one non-empty element. + * + * @param remaining the remaining argument list + * @param operation the operation name (for error messages) + * @param console console for error output + * @return true if valid, false otherwise + */ + private boolean validateRemainingNotEmpty(List remaining, String operation, PrintStream console) { + if (!hasMinimumRemainingArgs(remaining, 1)) { + console.println("Error: ID is required for " + operation + " operation"); + console.println("Usage: unomi:crud " + operation + " "); + console.println("Example: unomi:crud " + operation + " goal test-goal-123"); + return false; + } + return true; + } + + /** + * Handle read operation. + * Syntax: unomi:crud read + * remaining[0] = Object ID + */ + private Object handleRead(CrudCommand cmd, PrintStream console) throws Exception { + if (!validateRemainingNotEmpty(remaining, "read", console)) { + return null; + } + + String id = remaining.get(0); + Map obj = cmd.read(id); + if (obj != null) { + console.println(OBJECT_MAPPER.writerWithDefaultPrettyPrinter().writeValueAsString(obj)); + } else { + console.println(type + " not found with ID: " + id); + } + return null; + } + + /** + * Handle update operation. + * Syntax: unomi:crud update + * remaining[0] = Object ID + * remaining[1] = JSON string or URL + */ + private Object handleUpdate(CrudCommand cmd, PrintStream console) throws Exception { + if (!hasMinimumRemainingArgs(remaining, 2)) { + console.println("Error: ID and JSON/URL are required for update operation"); + console.println("Usage: unomi:crud update "); + console.println("Example: unomi:crud update goal test-goal-123 '{\"itemId\":\"test-goal-123\",\"enabled\":false}'"); + console.println("Note: Quote JSON strings to ensure they're treated as a single argument"); + return null; + } + + String id = remaining.get(0); + String jsonOrUrl = remaining.get(1); + + // hasMinimumRemainingArgs already ensures both id and jsonOrUrl are non-blank + Map updateProps = parsePropertiesWithErrorHandling(jsonOrUrl, console); + if (updateProps == null) { + return null; + } + + cmd.update(id, updateProps); + console.println("Updated " + type + " with ID: " + id); + return null; + } + + /** + * Handle delete operation. + * Syntax: unomi:crud delete + * remaining[0] = Object ID + */ + private Object handleDelete(CrudCommand cmd, PrintStream console) throws Exception { + if (!validateRemainingNotEmpty(remaining, "delete", console)) { + return null; + } + + String id = remaining.get(0); + cmd.delete(id); + console.println("Deleted " + type + " with ID: " + id); + return null; + } + + /** + * Handle list operation. + * Syntax: unomi:crud list [--csv] [-n ] + * remaining contains all remaining tokens (--csv, -n, 50, etc.) for manual parsing + */ + private Object handleList(CrudCommand cmd, PrintStream console) throws Exception { + // Parse list-specific options from remaining argument + parseListOptions(remaining); + + String[] headers = cmd.getHeaders(); + if (headers == null || headers.length == 0) { + console.println("Error: No headers available for " + type); + return null; + } + + // Ensure limit is positive (default to 100 if null or invalid) + int limit = (maxEntries != null && maxEntries > 0) ? maxEntries : 100; + + if (csv) { + // Generate proper CSV output using Apache Commons CSV + cmd.buildCsvOutput(console, headers, limit); + } else { + // Generate table output + ShellTable table = new ShellTable(); + for (String header : headers) { + table.column(header); + } + cmd.buildRows(table, limit); + table.print(console, true); + } + return null; + } +} diff --git a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/BaseCommand.java b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/BaseCommand.java new file mode 100644 index 0000000000..a968aaff1b --- /dev/null +++ b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/BaseCommand.java @@ -0,0 +1,53 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.unomi.shell.dev.commands; + +import org.apache.karaf.shell.api.action.Action; +import org.apache.karaf.shell.api.action.lifecycle.Reference; +import org.apache.karaf.shell.api.console.Session; +import org.apache.karaf.shell.support.table.ShellTable; +import org.apache.unomi.persistence.spi.PersistenceService; + +import java.io.IOException; +import java.io.PrintStream; + +/** + * Base class for Unomi shell commands + */ +public abstract class BaseCommand implements Action { + protected static final int DEFAULT_ENTRIES = 100; + + @Reference + protected PersistenceService persistenceService; + + protected ShellTable buildTable() { + ShellTable table = new ShellTable(); + table.column("ID"); + table.column("Name"); + table.column("Description"); + return table; + } + + protected void printTable(ShellTable table, Session session) { + table.print(session.getConsole()); + } + + protected boolean confirm(Session session, String message) throws IOException { + String response = session.readLine(message + " (y/n): ", null); + return response != null && response.toLowerCase().startsWith("y"); + } +} diff --git a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/BaseListCommand.java b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/BaseListCommand.java new file mode 100644 index 0000000000..f40b51a359 --- /dev/null +++ b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/BaseListCommand.java @@ -0,0 +1,74 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.unomi.shell.dev.commands; + +import org.apache.karaf.shell.api.action.Option; +import org.apache.karaf.shell.api.action.lifecycle.Reference; +import org.apache.karaf.shell.api.console.Session; +import org.apache.karaf.shell.support.table.ShellTable; +import org.apache.unomi.api.Item; +import org.apache.unomi.api.PartialList; +import org.apache.unomi.api.conditions.Condition; +import org.apache.unomi.api.query.Query; +import org.apache.unomi.api.services.DefinitionsService; + +/** + * Base class for list commands + */ +public abstract class BaseListCommand extends BaseCommand { + + @Reference + protected DefinitionsService definitionsService; + + @Reference + protected Session session; + + @Option(name = "--max-entries", description = "Maximum number of entries to display", required = false) + protected int maxEntries = DEFAULT_ENTRIES; + + @Option(name = "--sort-by", description = "Sort by field name", required = false) + protected String sortBy; + + protected abstract Class getItemType(); + + protected abstract void printItem(ShellTable table, T item); + + @Override + public Object execute() throws Exception { + Query query = new Query(); + query.setLimit(maxEntries); + query.setSortby(sortBy); + + Condition condition = new Condition(); + condition.setConditionType(definitionsService.getConditionType("matchAllCondition")); + query.setCondition(condition); + + PartialList items = persistenceService.query(query.getCondition(), query.getSortby(), getItemType(), query.getOffset(), query.getLimit()); + + ShellTable table = buildTable(); + for (T item : items.getList()) { + printItem(table, item); + } + printTable(table, session); + + return null; + } + + public void setDefinitionsService(DefinitionsService definitionsService) { + this.definitionsService = definitionsService; + } +} diff --git a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/BaseSimpleCommand.java b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/BaseSimpleCommand.java new file mode 100644 index 0000000000..1ef8ee99bc --- /dev/null +++ b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/BaseSimpleCommand.java @@ -0,0 +1,61 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.unomi.shell.dev.commands; + +import org.apache.karaf.shell.api.action.Action; +import org.apache.karaf.shell.api.action.lifecycle.Reference; +import org.apache.karaf.shell.api.console.Session; + +import java.io.PrintStream; + +/** + * Base class for simple shell commands that provides common functionality + * for accessing Session and console output. + */ +public abstract class BaseSimpleCommand implements Action { + + @Reference + protected Session session; + + /** + * Get the console PrintStream from the session. + * + * @return the console PrintStream + */ + protected PrintStream getConsole() { + return session.getConsole(); + } + + /** + * Print a message to the console. + * + * @param message the message to print + */ + protected void println(String message) { + getConsole().println(message); + } + + /** + * Print a formatted message to the console. + * + * @param format the format string + * @param args the arguments + */ + protected void printf(String format, Object... args) { + getConsole().printf(format, args); + } +} diff --git a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/CommandUtils.java b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/CommandUtils.java new file mode 100644 index 0000000000..44dfcbf821 --- /dev/null +++ b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/CommandUtils.java @@ -0,0 +1,80 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.unomi.shell.dev.commands; + +import java.text.SimpleDateFormat; +import java.util.Date; + +/** + * Utility class for common command functionality. + */ +public final class CommandUtils { + + /** + * Standard date format used across commands: "yyyy-MM-dd HH:mm:ss" + */ + public static final String DATE_FORMAT_PATTERN = "yyyy-MM-dd HH:mm:ss"; + + /** + * Thread-local SimpleDateFormat instance for date formatting. + * SimpleDateFormat is not thread-safe, so we use ThreadLocal to ensure thread safety. + */ + private static final ThreadLocal DATE_FORMAT = + ThreadLocal.withInitial(() -> new SimpleDateFormat(DATE_FORMAT_PATTERN)); + + private CommandUtils() { + // Utility class - prevent instantiation + } + + /** + * Format a date using the standard date format pattern. + * + * @param date the date to format + * @return the formatted date string, or "-" if date is null + */ + public static String formatDate(Date date) { + if (date == null) { + return "-"; + } + return DATE_FORMAT.get().format(date); + } + + /** + * Format a date using the standard date format pattern. + * + * @param date the date to format + * @param nullValue the value to return if date is null + * @return the formatted date string, or nullValue if date is null + */ + public static String formatDate(Date date, String nullValue) { + if (date == null) { + return nullValue; + } + return DATE_FORMAT.get().format(date); + } + + /** + * Get a SimpleDateFormat instance for the standard pattern. + * Note: This returns a new instance each time. For thread-safe usage, + * prefer using formatDate() methods. + * + * @return a SimpleDateFormat instance + */ + public static SimpleDateFormat getDateFormat() { + return new SimpleDateFormat(DATE_FORMAT_PATTERN); + } +} diff --git a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/DeployDefinition.java b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/DeployDefinition.java new file mode 100644 index 0000000000..243e04e518 --- /dev/null +++ b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/DeployDefinition.java @@ -0,0 +1,137 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.unomi.shell.dev.commands; + +import org.apache.karaf.shell.api.action.Command; +import org.apache.karaf.shell.api.action.lifecycle.Service; +import org.apache.unomi.api.Patch; +import org.apache.unomi.api.PersonaWithSessions; +import org.apache.unomi.api.PropertyType; +import org.apache.unomi.api.actions.ActionType; +import org.apache.unomi.api.campaigns.Campaign; +import org.apache.unomi.api.conditions.ConditionType; +import org.apache.unomi.api.goals.Goal; +import org.apache.unomi.api.rules.Rule; +import org.apache.unomi.api.segments.Scoring; +import org.apache.unomi.api.segments.Segment; + +import java.io.IOException; +import java.io.PrintStream; +import java.net.URL; + +@Command(scope = "unomi", name = "deploy-definition", description = "This will deploy Unomi definitions contained in bundles") +@Service +public class DeployDefinition extends DeploymentCommandSupport { + + public void processDefinition(String definitionType, URL definitionURL) { + try { + processDefinitionInternal(definitionType, definitionURL, getConsole(), "Predefined definition registered"); + } catch (IOException e) { + handleDefinitionError(definitionURL, "saving", e); + } + } + + protected void deployConditionType(URL definitionURL) throws IOException { + ConditionType conditionType = readDefinition(definitionURL, ConditionType.class); + definitionsService.setConditionType(conditionType); + } + + protected void deployActionType(URL definitionURL) throws IOException { + ActionType actionType = readDefinition(definitionURL, ActionType.class); + definitionsService.setActionType(actionType); + } + + protected void deployGoal(URL definitionURL) throws IOException { + Goal goal = readDefinition(definitionURL, Goal.class); + goalsService.setGoal(goal); + } + + protected void deployCampaign(URL definitionURL) throws IOException { + Campaign campaign = readDefinition(definitionURL, Campaign.class); + goalsService.setCampaign(campaign); + } + + protected void deployPersona(URL definitionURL) throws IOException { + PersonaWithSessions persona = readDefinition(definitionURL, PersonaWithSessions.class); + profileService.savePersonaWithSessions(persona); + } + + protected void deployPropertyType(URL definitionURL) throws IOException { + PropertyType propertyType = readDefinition(definitionURL, PropertyType.class); + profileService.setPropertyTypeTarget(definitionURL, propertyType); + profileService.setPropertyType(propertyType); + } + + protected void deployRule(URL definitionURL) throws IOException { + Rule rule = readDefinition(definitionURL, Rule.class); + rulesService.setRule(rule); + } + + protected void deploySegment(URL definitionURL) throws IOException { + Segment segment = readDefinition(definitionURL, Segment.class); + segmentService.setSegmentDefinition(segment); + } + + protected void deployScoring(URL definitionURL) throws IOException { + Scoring scoring = readDefinition(definitionURL, Scoring.class); + segmentService.setScoringDefinition(scoring); + } + + protected void deployPatch(URL definitionURL) throws IOException { + Patch patch = readDefinition(definitionURL, Patch.class); + patchService.patch(patch); + } + + @Override + protected boolean processDefinitionByType(String definitionType, URL definitionURL, PrintStream console) throws IOException { + switch (definitionType) { + case CONDITION_DEFINITION_TYPE: + deployConditionType(definitionURL); + return true; + case ACTION_DEFINITION_TYPE: + deployActionType(definitionURL); + return true; + case GOAL_DEFINITION_TYPE: + deployGoal(definitionURL); + return true; + case CAMPAIGN_DEFINITION_TYPE: + deployCampaign(definitionURL); + return true; + case PERSONA_DEFINITION_TYPE: + deployPersona(definitionURL); + return true; + case PROPERTY_DEFINITION_TYPE: + deployPropertyType(definitionURL); + return true; + case RULE_DEFINITION_TYPE: + deployRule(definitionURL); + return true; + case SEGMENT_DEFINITION_TYPE: + deploySegment(definitionURL); + return true; + case SCORING_DEFINITION_TYPE: + deployScoring(definitionURL); + return true; + case PATCH_DEFINITION_TYPE: + deployPatch(definitionURL); + return true; + default: + console.println("Unrecognized definition type:" + definitionType); + return false; + } + } +} diff --git a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/DeploymentCommandSupport.java b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/DeploymentCommandSupport.java similarity index 72% rename from tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/DeploymentCommandSupport.java rename to tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/DeploymentCommandSupport.java index 56db909abf..7bfdf79af7 100644 --- a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/DeploymentCommandSupport.java +++ b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/DeploymentCommandSupport.java @@ -14,14 +14,17 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.unomi.shell.commands; +package org.apache.unomi.shell.dev.commands; import org.apache.commons.lang3.StringUtils; import org.apache.karaf.shell.api.action.Action; import org.apache.karaf.shell.api.action.Argument; import org.apache.karaf.shell.api.action.lifecycle.Reference; import org.apache.karaf.shell.api.console.Session; + +import java.io.PrintStream; import org.apache.unomi.api.services.*; +import org.apache.unomi.persistence.spi.CustomObjectMapper; import org.jline.reader.LineReader; import org.osgi.framework.Bundle; import org.osgi.framework.BundleContext; @@ -33,7 +36,7 @@ import java.util.stream.IntStream; import java.util.stream.Stream; -public abstract class DeploymentCommandSupport implements Action { +public abstract class DeploymentCommandSupport extends BaseSimpleCommand { public static final String ALL_OPTION_LABEL = "* (All)"; @Reference @@ -57,8 +60,6 @@ public abstract class DeploymentCommandSupport implements Action { @Reference BundleContext bundleContext; - @Reference - Session session; public static final String CONDITION_DEFINITION_TYPE = "conditions"; public static final String ACTION_DEFINITION_TYPE = "actions"; @@ -102,6 +103,74 @@ public abstract class DeploymentCommandSupport implements Action { public abstract void processDefinition(String definitionType, URL definitionURL); + /** + * Process definition by type. Override in subclasses to provide specific implementation. + * + * @param definitionType the type of definition + * @param definitionURL the URL of the definition + * @param console the console for output + * @return true if successful, false otherwise + * @throws IOException if there's an error reading the definition + */ + protected boolean processDefinitionByType(String definitionType, URL definitionURL, PrintStream console) throws IOException { + return false; + } + + /** + * Read a definition object from a URL using CustomObjectMapper. + * + * @param definitionURL the URL to read from + * @param clazz the class of the object to read + * @param the type of the object + * @return the read object + * @throws IOException if there's an error reading the definition + */ + protected T readDefinition(URL definitionURL, Class clazz) throws IOException { + return CustomObjectMapper.getObjectMapper().readValue(definitionURL, clazz); + } + + /** + * Handle errors that occur during definition processing. + * + * @param definitionURL the URL of the definition that caused the error + * @param operation the operation being performed (e.g., "saving", "removing") + * @param e the exception that occurred + */ + protected void handleDefinitionError(URL definitionURL, String operation, IOException e) { + PrintStream console = getConsole(); + console.println("Error while " + operation + " definition " + definitionURL); + console.println(e.getMessage()); + } + + /** + * Internal method to process a definition. Handles common logic like resolving definition type. + * + * @param definitionType the type of definition + * @param definitionURL the URL of the definition + * @param console the console for output + * @param successMessage the message to display on success + * @throws IOException if there's an error processing the definition + */ + protected void processDefinitionInternal(String definitionType, URL definitionURL, PrintStream console, String successMessage) throws IOException { + if (ALL_OPTION_LABEL.equals(definitionType)) { + String definitionURLString = definitionURL.toString(); + for (String possibleDefinitionType : definitionTypes) { + if (definitionURLString.contains(getDefinitionTypePath(possibleDefinitionType))) { + definitionType = possibleDefinitionType; + break; + } + } + if (ALL_OPTION_LABEL.equals(definitionType)) { + console.println("Couldn't resolve definition type for definition URL " + definitionURL); + return; + } + } + boolean successful = processDefinitionByType(definitionType, definitionURL, console); + if (successful) { + console.println(successMessage + " : " + definitionURL.getFile()); + } + } + public Object execute() throws Exception { List bundlesToUpdate; if ("*".equals(definitionType)) { @@ -137,7 +206,7 @@ public Object execute() throws Exception { Bundle bundle = bundleContext.getBundle(bundleIdentifier); if (bundle == null) { - System.out.println("Couldn't find a bundle with id: " + bundleIdentifier); + println("Couldn't find a bundle with id: " + bundleIdentifier); return null; } @@ -149,7 +218,7 @@ public Object execute() throws Exception { possibleDefinitionNames.add(ALL_OPTION_LABEL); if (possibleDefinitionNames.isEmpty()) { - System.out.println("Couldn't find definitions in bundle : " + bundlesToUpdate); + println("Couldn't find definitions in bundle : " + bundlesToUpdate); return null; } @@ -159,14 +228,14 @@ public Object execute() throws Exception { } if (!definitionTypes.contains(definitionType) && !ALL_OPTION_LABEL.equals(definitionType)) { - System.out.println("Invalid type '" + definitionType + "' , allowed values : " +definitionTypes); + println("Invalid type '" + definitionType + "' , allowed values : " +definitionTypes); return null; } String definitionTypePath = getDefinitionTypePath(definitionType); List definitionTypeURLs = bundlesToUpdate.stream().flatMap(b->b.findEntries(definitionTypePath, "*.json", true) != null ? Collections.list(b.findEntries(definitionTypePath, "*.json", true)).stream() : Stream.empty()).collect(Collectors.toList()); if (definitionTypeURLs.isEmpty()) { - System.out.println("Couldn't find definitions in bundle with id: " + bundleIdentifier + " and definition path: " + definitionTypePath); + println("Couldn't find definitions in bundle with id: " + bundleIdentifier + " and definition path: " + definitionTypePath); return null; } @@ -194,7 +263,7 @@ public Object execute() throws Exception { URL url = optionalURL.get(); processDefinition(definitionType, url); } else { - System.out.println("Couldn't find file " + fileName); + println("Couldn't find file " + fileName); return null; } } diff --git a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/EventTail.java b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/EventTail.java similarity index 81% rename from tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/EventTail.java rename to tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/EventTail.java index 36ecdeceba..c4fd462c95 100644 --- a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/EventTail.java +++ b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/EventTail.java @@ -14,7 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.unomi.shell.commands; +package org.apache.unomi.shell.dev.commands; import org.apache.karaf.shell.api.action.Argument; import org.apache.karaf.shell.api.action.Command; @@ -24,7 +24,6 @@ import org.apache.unomi.api.services.EventService; import java.io.PrintStream; -import java.util.ArrayList; import java.util.List; @Command(scope = "unomi", name = "event-tail", description = "This will tail all the events coming into the Apache Unomi Context Server") @@ -57,12 +56,12 @@ public String[] getColumnHeaders() { @Override public Object getListener() { - return new TailEventListener(session.getConsole()); + return new TailEventListener(getConsole()); } class TailEventListener implements EventListenerService { - PrintStream out; + private final PrintStream out; public TailEventListener(PrintStream out) { this.out = out; @@ -78,14 +77,7 @@ public int onEvent(Event event) { if (!event.isPersistent() && !withInternal) { return EventService.NO_CHANGE; } - List eventInfo = new ArrayList<>(); - eventInfo.add(event.getItemId()); - eventInfo.add(event.getEventType()); - eventInfo.add(event.getSessionId()); - eventInfo.add(event.getProfileId()); - eventInfo.add(event.getTimeStamp().toString()); - eventInfo.add(event.getScope()); - eventInfo.add(Boolean.toString(event.isPersistent())); + List eventInfo = TailCommandUtils.extractEventInfo(event); outputLine(out, eventInfo); return EventService.NO_CHANGE; } diff --git a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/ListCommandSupport.java b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/ListCommandSupport.java similarity index 87% rename from tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/ListCommandSupport.java rename to tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/ListCommandSupport.java index dad45f10b7..154c3c96d5 100644 --- a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/ListCommandSupport.java +++ b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/ListCommandSupport.java @@ -14,12 +14,16 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.unomi.shell.commands; +package org.apache.unomi.shell.dev.commands; import org.apache.karaf.shell.api.action.Action; import org.apache.karaf.shell.api.action.Option; +import org.apache.karaf.shell.api.action.lifecycle.Reference; +import org.apache.karaf.shell.api.console.Session; import org.apache.karaf.shell.support.table.Row; import org.apache.karaf.shell.support.table.ShellTable; + +import java.io.PrintStream; import org.apache.unomi.common.DataTable; import java.util.ArrayList; @@ -29,6 +33,9 @@ */ public abstract class ListCommandSupport implements Action { + @Reference + protected Session session; + @Option(name = "--csv", description = "Output table in CSV format", required = false, multiValued = false) boolean csv; @@ -47,13 +54,13 @@ public abstract class ListCommandSupport implements Action { protected abstract DataTable buildDataTable(); public Object execute() throws Exception { - DataTable dataTable = buildDataTable(); String[] headers = getHeaders(); + PrintStream console = session.getConsole(); if (csv) { - System.out.println(dataTable.toCSV(headers)); + console.println(dataTable.toCSV(headers)); return null; } @@ -70,7 +77,7 @@ public Object execute() throws Exception { row.addContent(rowData); } - shellTable.print(System.out); + shellTable.print(console); return null; } diff --git a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/RemoveCommandSupport.java b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/RemoveCommandSupport.java new file mode 100644 index 0000000000..58009cca66 --- /dev/null +++ b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/RemoveCommandSupport.java @@ -0,0 +1,51 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.unomi.shell.dev.commands; + +import org.apache.karaf.shell.api.action.Action; +import org.apache.karaf.shell.api.action.Option; + +import java.io.IOException; + +public abstract class RemoveCommandSupport extends BaseSimpleCommand { + + @Option(name = "--force", description = "Force deletion without confirmation", required = false, multiValued = false) + boolean force; + + public abstract Object doRemove() throws Exception; + + public abstract String getResourceDescription(); + + @Override + public Object execute() throws Exception { + Object result = null; + // Prompt for confirmation + if (force || askForConfirmation("Are you sure you want to delete "+getResourceDescription()+" ? (yes/no): ")) { + result = doRemove(); + println("Resource deleted successfully."); + } else { + println("Operation cancelled."); + } + return result; + } + + private boolean askForConfirmation(String prompt) throws IOException { + String input = session.readLine(prompt, null); + return "yes".equals(input); + } + +} diff --git a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/RuleResetStats.java b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/RuleResetStats.java similarity index 86% rename from tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/RuleResetStats.java rename to tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/RuleResetStats.java index 7a228be9fa..ad5c11aba2 100644 --- a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/RuleResetStats.java +++ b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/RuleResetStats.java @@ -14,9 +14,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.unomi.shell.commands; +package org.apache.unomi.shell.dev.commands; -import org.apache.karaf.shell.api.action.Action; import org.apache.karaf.shell.api.action.Command; import org.apache.karaf.shell.api.action.lifecycle.Reference; import org.apache.karaf.shell.api.action.lifecycle.Service; @@ -24,7 +23,7 @@ @Command(scope = "unomi", name = "rule-reset-stats", description = "This command will reset the rule statistics") @Service -public class RuleResetStats implements Action { +public class RuleResetStats extends BaseSimpleCommand { @Reference RulesService rulesService; @@ -32,7 +31,7 @@ public class RuleResetStats implements Action { @Override public Object execute() throws Exception { rulesService.resetAllRuleStatistics(); - System.out.println("Rule statistics successfully reset."); + println("Rule statistics successfully reset."); return null; } } diff --git a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/RuleTail.java b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/RuleTail.java similarity index 80% rename from tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/RuleTail.java rename to tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/RuleTail.java index 6cf66b2192..e6de275a94 100644 --- a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/RuleTail.java +++ b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/RuleTail.java @@ -14,7 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.unomi.shell.commands; +package org.apache.unomi.shell.dev.commands; import org.apache.karaf.shell.api.action.Command; import org.apache.karaf.shell.api.action.lifecycle.Service; @@ -23,7 +23,6 @@ import org.apache.unomi.api.services.RuleListenerService; import java.io.PrintStream; -import java.util.ArrayList; import java.util.List; /** @@ -56,12 +55,12 @@ public String[] getColumnHeaders() { @Override public Object getListener() { - return new TailRuleListener(session.getConsole()); + return new TailRuleListener(getConsole()); } class TailRuleListener implements RuleListenerService { - PrintStream out; + private final PrintStream out; public TailRuleListener(PrintStream out) { this.out = out; @@ -79,14 +78,7 @@ public void onAlreadyRaised(AlreadyRaisedFor alreadyRaisedFor, Rule rule, Event @Override public void onExecuteActions(Rule rule, Event event) { - List ruleExecutionInfo = new ArrayList<>(); - ruleExecutionInfo.add(rule.getItemId()); - ruleExecutionInfo.add(rule.getMetadata().getName()); - ruleExecutionInfo.add(event.getEventType()); - ruleExecutionInfo.add(event.getSessionId()); - ruleExecutionInfo.add(event.getProfileId()); - ruleExecutionInfo.add(event.getTimeStamp().toString()); - ruleExecutionInfo.add(event.getScope()); + List ruleExecutionInfo = TailCommandUtils.extractRuleExecutionInfo(rule, event); outputLine(out, ruleExecutionInfo); } } diff --git a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/RuleWatch.java b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/RuleWatch.java similarity index 82% rename from tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/RuleWatch.java rename to tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/RuleWatch.java index c94394455c..63ba069e59 100644 --- a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/RuleWatch.java +++ b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/RuleWatch.java @@ -14,7 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.unomi.shell.commands; +package org.apache.unomi.shell.dev.commands; import org.apache.commons.lang3.ArrayUtils; import org.apache.karaf.shell.api.action.Argument; @@ -25,7 +25,6 @@ import org.apache.unomi.api.services.RuleListenerService; import java.io.PrintStream; -import java.util.ArrayList; import java.util.List; /** @@ -62,12 +61,12 @@ public String[] getColumnHeaders() { @Override public Object getListener() { - return new RuleWatchListener(session.getConsole()); + return new RuleWatchListener(getConsole()); } class RuleWatchListener implements RuleListenerService { - PrintStream out; + private final PrintStream out; public RuleWatchListener(PrintStream out) { this.out = out; @@ -78,7 +77,6 @@ public void onEvaluate(Rule rule, Event event) { populateRuleInfo(rule, event, "EVALUATE"); } - @Override public void onAlreadyRaised(AlreadyRaisedFor alreadyRaisedFor, Rule rule, Event event) { populateRuleInfo(rule, event, "AR " + alreadyRaisedFor.toString()); @@ -93,15 +91,7 @@ public void populateRuleInfo(Rule rule, Event event, String status) { if (!ArrayUtils.contains(ruleIds, rule.getItemId())) { return; } - List ruleExecutionInfo = new ArrayList<>(); - ruleExecutionInfo.add(status); - ruleExecutionInfo.add(rule.getItemId()); - ruleExecutionInfo.add(rule.getMetadata().getName()); - ruleExecutionInfo.add(event.getEventType()); - ruleExecutionInfo.add(event.getSessionId()); - ruleExecutionInfo.add(event.getProfileId()); - ruleExecutionInfo.add(event.getTimeStamp().toString()); - ruleExecutionInfo.add(event.getScope()); + List ruleExecutionInfo = TailCommandUtils.extractRuleExecutionInfoWithStatus(rule, event, status); outputLine(out, ruleExecutionInfo); } diff --git a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/TailCommandSupport.java b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/TailCommandSupport.java similarity index 93% rename from tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/TailCommandSupport.java rename to tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/TailCommandSupport.java index f6af0328a4..7de0d44512 100644 --- a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/commands/TailCommandSupport.java +++ b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/TailCommandSupport.java @@ -14,12 +14,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.unomi.shell.commands; +package org.apache.unomi.shell.dev.commands; import org.apache.commons.lang3.StringUtils; import org.apache.karaf.shell.api.action.Action; import org.apache.karaf.shell.api.action.lifecycle.Reference; -import org.apache.karaf.shell.api.console.Session; import org.osgi.framework.BundleContext; import org.osgi.framework.ServiceRegistration; @@ -28,15 +27,12 @@ import java.util.Hashtable; import java.util.List; -public abstract class TailCommandSupport implements Action { +public abstract class TailCommandSupport extends BaseSimpleCommand { public abstract int[] getColumnSizes(); public abstract String[] getColumnHeaders(); - @Reference - Session session; - @Reference BundleContext bundleContext; @@ -74,12 +70,12 @@ public void outputLine(PrintStream out, List eventInfo) { } out.println(eventLine.toString()); } - + public abstract Object getListener(); public Object execute() throws Exception { // Do not use System.out as it may write to the wrong console depending on the thread that calls our log handler - PrintStream out = session.getConsole(); + PrintStream out = getConsole(); out.flush(); outputHeaders(out); diff --git a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/TailCommandUtils.java b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/TailCommandUtils.java new file mode 100644 index 0000000000..bb6fa50160 --- /dev/null +++ b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/TailCommandUtils.java @@ -0,0 +1,91 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.unomi.shell.dev.commands; + +import org.apache.unomi.api.Event; +import org.apache.unomi.api.rules.Rule; + +import java.util.ArrayList; +import java.util.List; + +/** + * Utility class for common tail command functionality. + */ +public final class TailCommandUtils { + + private TailCommandUtils() { + // Utility class - prevent instantiation + } + + /** + * Extract event information into a list of strings for display. + * + * @param event the event to extract information from + * @return list of event information strings + */ + public static List extractEventInfo(Event event) { + List eventInfo = new ArrayList<>(); + eventInfo.add(event.getItemId()); + eventInfo.add(event.getEventType()); + eventInfo.add(event.getSessionId()); + eventInfo.add(event.getProfileId()); + eventInfo.add(event.getTimeStamp().toString()); + eventInfo.add(event.getScope()); + eventInfo.add(Boolean.toString(event.isPersistent())); + return eventInfo; + } + + /** + * Extract rule execution information into a list of strings for display. + * + * @param rule the rule to extract information from + * @param event the event associated with the rule execution + * @return list of rule execution information strings + */ + public static List extractRuleExecutionInfo(Rule rule, Event event) { + List ruleExecutionInfo = new ArrayList<>(); + ruleExecutionInfo.add(rule.getItemId()); + ruleExecutionInfo.add(rule.getMetadata().getName()); + ruleExecutionInfo.add(event.getEventType()); + ruleExecutionInfo.add(event.getSessionId()); + ruleExecutionInfo.add(event.getProfileId()); + ruleExecutionInfo.add(event.getTimeStamp().toString()); + ruleExecutionInfo.add(event.getScope()); + return ruleExecutionInfo; + } + + /** + * Extract rule execution information with status into a list of strings for display. + * + * @param rule the rule to extract information from + * @param event the event associated with the rule execution + * @param status the status of the rule execution (e.g., "EVALUATE", "EXECUTE", "AR ...") + * @return list of rule execution information strings with status as first element + */ + public static List extractRuleExecutionInfoWithStatus(Rule rule, Event event, String status) { + List ruleExecutionInfo = new ArrayList<>(); + ruleExecutionInfo.add(status); + ruleExecutionInfo.add(rule.getItemId()); + ruleExecutionInfo.add(rule.getMetadata().getName()); + ruleExecutionInfo.add(event.getEventType()); + ruleExecutionInfo.add(event.getSessionId()); + ruleExecutionInfo.add(event.getProfileId()); + ruleExecutionInfo.add(event.getTimeStamp().toString()); + ruleExecutionInfo.add(event.getScope()); + return ruleExecutionInfo; + } +} diff --git a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/UndeployDefinition.java b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/UndeployDefinition.java new file mode 100644 index 0000000000..6ed184d2b3 --- /dev/null +++ b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/UndeployDefinition.java @@ -0,0 +1,97 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.unomi.shell.dev.commands; + +import org.apache.karaf.shell.api.action.Command; +import org.apache.karaf.shell.api.action.lifecycle.Service; +import org.apache.unomi.api.Patch; +import org.apache.unomi.api.PersonaWithSessions; +import org.apache.unomi.api.PropertyType; +import org.apache.unomi.api.actions.ActionType; +import org.apache.unomi.api.campaigns.Campaign; +import org.apache.unomi.api.conditions.ConditionType; +import org.apache.unomi.api.goals.Goal; +import org.apache.unomi.api.rules.Rule; +import org.apache.unomi.api.segments.Scoring; +import org.apache.unomi.api.segments.Segment; + +import java.io.IOException; +import java.io.PrintStream; +import java.net.URL; + +@Command(scope = "unomi", name = "undeploy-definition", description = "This will undeploy definitions contained in bundles") +@Service +public class UndeployDefinition extends DeploymentCommandSupport { + + public void processDefinition(String definitionType, URL definitionURL) { + try { + processDefinitionInternal(definitionType, definitionURL, getConsole(), "Predefined definition unregistered"); + } catch (IOException e) { + handleDefinitionError(definitionURL, "removing", e); + } + } + + @Override + protected boolean processDefinitionByType(String definitionType, URL definitionURL, PrintStream console) throws IOException { + switch (definitionType) { + case CONDITION_DEFINITION_TYPE: + ConditionType conditionType = readDefinition(definitionURL, ConditionType.class); + definitionsService.removeActionType(conditionType.getItemId()); + return true; + case ACTION_DEFINITION_TYPE: + ActionType actionType = readDefinition(definitionURL, ActionType.class); + definitionsService.removeActionType(actionType.getItemId()); + return true; + case GOAL_DEFINITION_TYPE: + Goal goal = readDefinition(definitionURL, Goal.class); + goalsService.removeGoal(goal.getItemId()); + return true; + case CAMPAIGN_DEFINITION_TYPE: + Campaign campaign = readDefinition(definitionURL, Campaign.class); + goalsService.removeCampaign(campaign.getItemId()); + return true; + case PERSONA_DEFINITION_TYPE: + PersonaWithSessions persona = readDefinition(definitionURL, PersonaWithSessions.class); + profileService.delete(persona.getPersona().getItemId(), true); + return true; + case PROPERTY_DEFINITION_TYPE: + PropertyType propertyType = readDefinition(definitionURL, PropertyType.class); + profileService.deletePropertyType(propertyType.getItemId()); + return true; + case RULE_DEFINITION_TYPE: + Rule rule = readDefinition(definitionURL, Rule.class); + rulesService.removeRule(rule.getItemId()); + return true; + case SEGMENT_DEFINITION_TYPE: + Segment segment = readDefinition(definitionURL, Segment.class); + segmentService.removeSegmentDefinition(segment.getItemId(), false); + return true; + case SCORING_DEFINITION_TYPE: + Scoring scoring = readDefinition(definitionURL, Scoring.class); + segmentService.removeScoringDefinition(scoring.getItemId(), false); + return true; + case PATCH_DEFINITION_TYPE: + Patch patch = readDefinition(definitionURL, Patch.class); + // patchService.patch(patch); + return true; + default: + console.println("Unrecognized definition type: " + definitionType); + return false; + } + } + +} diff --git a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/actions/ActionTypeCrudCommand.java b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/actions/ActionTypeCrudCommand.java new file mode 100644 index 0000000000..319464aef2 --- /dev/null +++ b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/actions/ActionTypeCrudCommand.java @@ -0,0 +1,157 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.unomi.shell.dev.commands.actions; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.unomi.api.Parameter; +import org.apache.unomi.api.PartialList; +import org.apache.unomi.api.Scope; +import org.apache.unomi.api.actions.ActionType; +import org.apache.unomi.api.query.Query; +import org.apache.unomi.api.services.ScopeService; +import org.apache.unomi.persistence.spi.CustomObjectMapper; +import org.apache.unomi.shell.dev.services.BaseCrudCommand; +import org.apache.unomi.shell.dev.services.CrudCommand; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +@Component(service = CrudCommand.class, immediate = true) +public class ActionTypeCrudCommand extends BaseCrudCommand { + + private static final ObjectMapper OBJECT_MAPPER = new CustomObjectMapper(); + private static final List PROPERTY_NAMES = List.of( + "itemId", "name", "description", "scope", "tags", "systemTags", "parameters" + ); + + @Reference + private ScopeService scopeService; + + @Override + public String getObjectType() { + return "actiontype"; + } + + @Override + protected String[] getHeadersWithoutTenant() { + return new String[] { + "Identifier", + "Name", + "Description", + "Scope", + "Tags", + "System Tags", + "Parameters", + "Action Executor" + }; + } + + @Override + protected PartialList getItems(Query query) { + List actionTypes = new ArrayList<>(definitionsService.getAllActionTypes()); + return paginateList(actionTypes, query); + } + + @Override + protected String[] buildRow(Object item) { + ActionType actionType = (ActionType) item; + return new String[] { + actionType.getItemId(), + actionType.getMetadata().getName(), + actionType.getMetadata().getDescription(), + actionType.getMetadata().getScope(), + String.join(",", actionType.getMetadata().getTags()), + String.join(",", actionType.getMetadata().getSystemTags()), + String.join(",", actionType.getParameters().stream().map(Parameter::getId).collect(Collectors.toList())), + actionType.getActionExecutor() + }; + } + + @Override + public String create(Map properties) { + ActionType actionType = OBJECT_MAPPER.convertValue(properties, ActionType.class); + definitionsService.setActionType(actionType); + return actionType.getItemId(); + } + + @Override + public Map read(String id) { + ActionType actionType = definitionsService.getActionType(id); + if (actionType != null) { + return OBJECT_MAPPER.convertValue(actionType, Map.class); + } + return null; + } + + @Override + public void update(String id, Map properties) { + ActionType existing = definitionsService.getActionType(id); + if (existing == null) { + throw new IllegalArgumentException("Action type not found: " + id); + } + + ActionType updated = OBJECT_MAPPER.convertValue(properties, ActionType.class); + updated.setItemId(id); + definitionsService.setActionType(updated); + } + + @Override + public void delete(String id) { + definitionsService.removeActionType(id); + } + + @Override + public String getPropertiesHelp() { + return String.join("\n", + "Required properties:", + "- itemId: Identifier for the action type", + "- name: Name of the action type", + "", + "Optional properties:", + "- description: Description of the action type", + "- scope: Scope of the action type", + "- tags: List of tags", + "- systemTags: List of system tags", + "- parameters: Map of parameter definitions" + ); + } + + @Override + public List completePropertyNames(String prefix) { + return filterPropertyNames(PROPERTY_NAMES, prefix); + } + + @Override + public List completePropertyValue(String propertyName, String prefix) { + if ("scope".equals(propertyName)) { + return scopeService.getScopes().stream() + .map(Scope::getItemId) + .filter(id -> id.startsWith(prefix)) + .collect(Collectors.toList()); + } + return Collections.emptyList(); + } + + public void setScopeService(ScopeService scopeService) { + this.scopeService = scopeService; + } +} diff --git a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/campaigns/CampaignCrudCommand.java b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/campaigns/CampaignCrudCommand.java new file mode 100644 index 0000000000..caa609f474 --- /dev/null +++ b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/campaigns/CampaignCrudCommand.java @@ -0,0 +1,151 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.unomi.shell.dev.commands.campaigns; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.unomi.api.PartialList; +import org.apache.unomi.api.campaigns.Campaign; +import org.apache.unomi.api.campaigns.CampaignDetail; +import org.apache.unomi.api.goals.Goal; +import org.apache.unomi.api.query.Query; +import org.apache.unomi.api.services.GoalsService; +import org.apache.unomi.persistence.spi.CustomObjectMapper; +import org.apache.unomi.shell.dev.services.BaseCrudCommand; +import org.apache.unomi.shell.dev.services.CrudCommand; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; + +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * A command to perform CRUD operations on campaigns + */ +@Component(service = CrudCommand.class, immediate = true) +public class CampaignCrudCommand extends BaseCrudCommand { + + private static final ObjectMapper OBJECT_MAPPER = new CustomObjectMapper(); + private static final List PROPERTY_NAMES = List.of( + "itemId", "name", "description", "scope", "startDate", "endDate", "cost", "currency", "primaryGoal", "goals", "entryCondition", "enabled" + ); + + @Reference + private GoalsService goalsService; + + @Override + public String getObjectType() { + return "campaign"; + } + + @Override + protected String[] getHeadersWithoutTenant() { + return new String[]{"ID", "Name", "Description", "Start Date", "End Date", "Cost", "Currency", "Primary Goal", "Enabled"}; + } + + @Override + protected PartialList getItems(Query query) { + return goalsService.getCampaignDetails(query); + } + + @Override + protected Comparable[] buildRow(Object item) { + CampaignDetail detail = (CampaignDetail) item; + Campaign campaign = detail.getCampaign(); + String primaryGoalName = ""; + if (campaign.getPrimaryGoal() != null) { + // Get the goal details to get its name + Goal primaryGoal = goalsService.getGoal(campaign.getPrimaryGoal()); + if (primaryGoal != null) { + primaryGoalName = primaryGoal.getMetadata().getName(); + } + } + return new Comparable[]{ + campaign.getItemId(), + campaign.getMetadata().getName(), + campaign.getMetadata().getDescription(), + campaign.getStartDate() != null ? campaign.getStartDate().toString() : "", + campaign.getEndDate() != null ? campaign.getEndDate().toString() : "", + campaign.getCost() != null ? campaign.getCost().toString() : "", + campaign.getCurrency(), + primaryGoalName, + campaign.getMetadata().isEnabled() + }; + } + + @Override + public Map read(String id) { + Campaign campaign = goalsService.getCampaign(id); + if (campaign == null) { + return null; + } + return OBJECT_MAPPER.convertValue(campaign, Map.class); + } + + @Override + public String create(Map properties) { + Campaign campaign = OBJECT_MAPPER.convertValue(properties, Campaign.class); + goalsService.setCampaign(campaign); + return campaign.getItemId(); + } + + @Override + public void update(String id, Map properties) { + Campaign existingCampaign = goalsService.getCampaign(id); + if (existingCampaign == null) { + return; + } + + Campaign updatedCampaign = OBJECT_MAPPER.convertValue(properties, Campaign.class); + updatedCampaign.setItemId(id); + goalsService.setCampaign(updatedCampaign); + } + + @Override + public void delete(String id) { + Campaign campaign = goalsService.getCampaign(id); + if (campaign != null) { + goalsService.removeCampaign(id); + } + } + + @Override + public List completePropertyNames(String prefix) { + return filterPropertyNames(PROPERTY_NAMES, prefix); + } + + @Override + public String getPropertiesHelp() { + return String.join("\n", + "Required properties:", + "- itemId: The unique identifier of the campaign", + "- name: The name of the campaign", + "- description: The description of the campaign", + "", + "Optional properties:", + "- scope: The scope of the campaign (defaults to systemscope)", + "- startDate: The start date of the campaign (ISO-8601 format)", + "- endDate: The end date of the campaign (ISO-8601 format)", + "- cost: The cost of the campaign", + "- currency: The currency for the campaign cost", + "- primaryGoal: The primary goal of the campaign", + "- goals: List of goals associated with the campaign", + "- entryCondition: The condition that determines when a visitor enters the campaign", + "- enabled: Whether the campaign is enabled (true/false)" + ); + } +} diff --git a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/campaigns/CampaignEventCrudCommand.java b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/campaigns/CampaignEventCrudCommand.java new file mode 100644 index 0000000000..8096e9dcb9 --- /dev/null +++ b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/campaigns/CampaignEventCrudCommand.java @@ -0,0 +1,143 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.unomi.shell.dev.commands.campaigns; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.unomi.api.PartialList; +import org.apache.unomi.api.campaigns.events.CampaignEvent; +import org.apache.unomi.api.conditions.Condition; +import org.apache.unomi.api.query.Query; +import org.apache.unomi.api.services.GoalsService; +import org.apache.unomi.persistence.spi.CustomObjectMapper; +import org.apache.unomi.shell.dev.services.BaseCrudCommand; +import org.apache.unomi.shell.dev.services.CrudCommand; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * A command to perform CRUD operations on campaign events + */ +@Component(service = CrudCommand.class, immediate = true) +public class CampaignEventCrudCommand extends BaseCrudCommand { + + private static final ObjectMapper OBJECT_MAPPER = new CustomObjectMapper(); + private static final List PROPERTY_NAMES = List.of( + "itemId", "name", "description", "scope", "campaignId", "eventDate", "cost", "currency", "timezone" + ); + + @Reference + private GoalsService goalsService; + + @Override + public String getObjectType() { + return "campaignevent"; + } + + @Override + protected String[] getHeadersWithoutTenant() { + return new String[]{"ID", "Name", "Description", "Campaign ID", "Event Date", "Cost", "Currency", "Timezone"}; + } + + @Override + protected PartialList getItems(Query query) { + return goalsService.getEvents(query); + } + + @Override + protected Comparable[] buildRow(Object item) { + CampaignEvent event = (CampaignEvent) item; + return new Comparable[]{ + event.getItemId(), + event.getMetadata().getName(), + event.getMetadata().getDescription(), + event.getCampaignId(), + event.getEventDate() != null ? event.getEventDate().toString() : "", + event.getCost() != null ? event.getCost().toString() : "", + event.getCurrency(), + event.getTimezone() + }; + } + + @Override + public Map read(String id) { + // There's no direct method to get a single campaign event, so we need to query for it + Query query = new Query(); + Condition condition = new Condition(); + condition.setConditionType(definitionsService.getConditionType("matchAllCondition")); + condition.setParameter("operator", "and"); + condition.setParameter("subConditions", new ArrayList<>()); + query.setCondition(condition); + + PartialList events = goalsService.getEvents(query); + CampaignEvent event = events.getList().stream() + .filter(e -> e.getItemId().equals(id)) + .findFirst() + .orElse(null); + + if (event == null) { + return null; + } + return OBJECT_MAPPER.convertValue(event, Map.class); + } + + @Override + public String create(Map properties) { + CampaignEvent event = OBJECT_MAPPER.convertValue(properties, CampaignEvent.class); + goalsService.setCampaignEvent(event); + return event.getItemId(); + } + + @Override + public void update(String id, Map properties) { + CampaignEvent updatedEvent = OBJECT_MAPPER.convertValue(properties, CampaignEvent.class); + updatedEvent.setItemId(id); + goalsService.setCampaignEvent(updatedEvent); + } + + @Override + public void delete(String id) { + goalsService.removeCampaignEvent(id); + } + + @Override + public List completePropertyNames(String prefix) { + return filterPropertyNames(PROPERTY_NAMES, prefix); + } + + @Override + public String getPropertiesHelp() { + return String.join("\n", + "Required properties:", + "- itemId: The unique identifier of the campaign event", + "- name: The name of the campaign event", + "- description: The description of the campaign event", + "- campaignId: The ID of the campaign this event belongs to", + "", + "Optional properties:", + "- scope: The scope of the campaign event (defaults to systemscope)", + "- eventDate: The date of the event (ISO-8601 format)", + "- cost: The cost associated with this event", + "- currency: The currency for the event cost", + "- timezone: The timezone for the event" + ); + } +} diff --git a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/conditions/ConditionTypeCrudCommand.java b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/conditions/ConditionTypeCrudCommand.java new file mode 100644 index 0000000000..b98560290a --- /dev/null +++ b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/conditions/ConditionTypeCrudCommand.java @@ -0,0 +1,153 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.unomi.shell.dev.commands.conditions; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.commons.lang3.StringUtils; +import org.apache.unomi.api.Metadata; +import org.apache.unomi.api.PartialList; +import org.apache.unomi.api.conditions.ConditionType; +import org.apache.unomi.api.query.Query; +import org.apache.unomi.persistence.spi.CustomObjectMapper; +import org.apache.unomi.shell.dev.services.BaseCrudCommand; +import org.apache.unomi.shell.dev.services.CrudCommand; +import org.osgi.service.component.annotations.Component; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +@Component(service = CrudCommand.class, immediate = true) +public class ConditionTypeCrudCommand extends BaseCrudCommand { + + private static final ObjectMapper OBJECT_MAPPER = new CustomObjectMapper(); + private static final List PROPERTY_NAMES = List.of( + "itemId", "scope", "name", "description", "conditionEvaluator", "queryBuilder", "parameters", "parentCondition" + ); + + @Override + public String getObjectType() { + return "conditiontype"; + } + + @Override + protected String[] getHeadersWithoutTenant() { + return new String[] { + "Activated", + "Hidden", + "Read-only", + "Identifier", + "Scope", + "Name", + "Tags", + "System tags" + }; + } + + @Override + protected PartialList getItems(Query query) { + List allTypes = new ArrayList<>(definitionsService.getAllConditionTypes()); + int offset = query.getOffset(); + int pageSize = query.getLimit(); + int totalSize = allTypes.size(); + + List pageTypes = allTypes.subList( + Math.min(offset, totalSize), + Math.min(offset + pageSize, totalSize) + ); + + return new PartialList(pageTypes, offset, pageSize, totalSize, PartialList.Relation.EQUAL); + } + + @Override + protected Comparable[] buildRow(Object item) { + ConditionType type = (ConditionType) item; + Metadata metadata = type.getMetadata(); + ArrayList rowData = new ArrayList<>(); + rowData.add(metadata.isEnabled() ? "x" : ""); + rowData.add(metadata.isHidden() ? "x" : ""); + rowData.add(metadata.isReadOnly() ? "x" : ""); + rowData.add(metadata.getId()); + rowData.add(metadata.getScope()); + rowData.add(metadata.getName()); + rowData.add(StringUtils.join(metadata.getTags(), ",")); + rowData.add(StringUtils.join(metadata.getSystemTags(), ",")); + return rowData.toArray(new Comparable[0]); + } + + @Override + public String create(Map properties) { + ConditionType conditionType = OBJECT_MAPPER.convertValue(properties, ConditionType.class); + definitionsService.setConditionType(conditionType); + return conditionType.getItemId(); + } + + @Override + public Map read(String id) { + ConditionType conditionType = definitionsService.getConditionType(id); + if (conditionType == null) { + return null; + } + return OBJECT_MAPPER.convertValue(conditionType, Map.class); + } + + @Override + public void update(String id, Map properties) { + ConditionType conditionType = definitionsService.getConditionType(id); + if (conditionType == null) { + throw new IllegalArgumentException("Condition type with id '" + id + "' not found"); + } + ConditionType updatedConditionType = OBJECT_MAPPER.convertValue(properties, ConditionType.class); + updatedConditionType.setItemId(id); + definitionsService.setConditionType(updatedConditionType); + } + + @Override + public void delete(String id) { + definitionsService.removeConditionType(id); + } + + @Override + public List completePropertyNames(String prefix) { + return filterPropertyNames(PROPERTY_NAMES, prefix); + } + + @Override + public String getPropertiesHelp() { + return "Required properties:\n" + + "- itemId (string): Unique identifier for the condition type\n" + + "- scope (string): Scope of the condition type\n" + + "- name (string): Human-readable name\n" + + "\n" + + "Optional properties:\n" + + "- description (string): Description of the condition type\n" + + "- conditionEvaluator (string): Name of the condition evaluator implementation\n" + + "- queryBuilder (string): Name of the query builder implementation\n" + + "- parameters (array): List of parameters, each containing:\n" + + " - id (string): Parameter identifier\n" + + " - type (string): Parameter type\n" + + " - multivalued (boolean): Whether the parameter accepts multiple values\n" + + " - defaultValue (any): Default value for the parameter\n" + + "- parentCondition (object): Parent condition definition\n" + + "- enabled (boolean): Whether the condition type is enabled (default: true)\n" + + "- hidden (boolean): Whether the condition type is hidden (default: false)\n" + + "- readOnly (boolean): Whether the condition type is read-only (default: false)\n" + + "- tags (array): List of tags for the condition type\n" + + "- systemTags (array): List of system tags for the condition type"; + } +} diff --git a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/consents/ConsentCrudCommand.java b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/consents/ConsentCrudCommand.java new file mode 100644 index 0000000000..ed59c42882 --- /dev/null +++ b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/consents/ConsentCrudCommand.java @@ -0,0 +1,244 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.unomi.shell.dev.commands.consents; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.unomi.api.Consent; +import org.apache.unomi.api.PartialList; +import org.apache.unomi.api.Profile; +import org.apache.unomi.api.query.Query; +import org.apache.unomi.api.services.ProfileService; +import org.apache.unomi.persistence.spi.CustomObjectMapper; +import org.apache.unomi.shell.dev.services.BaseCrudCommand; +import org.apache.unomi.shell.dev.services.CrudCommand; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; + +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * A command to perform CRUD operations on consents + */ +@Component(service = CrudCommand.class, immediate = true) +public class ConsentCrudCommand extends BaseCrudCommand { + + private static final ObjectMapper OBJECT_MAPPER = new CustomObjectMapper(); + private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ"); + private static final List PROPERTY_NAMES = List.of( + "profileId", "scope", "typeIdentifier", "status", "statusDate", "revokeDate" + ); + + @Reference + private ProfileService profileService; + + @Override + public String getObjectType() { + return "consent"; + } + + @Override + protected String[] getHeadersWithoutTenant() { + return new String[]{"Profile ID", "Scope", "Type", "Status", "Status Date", "Revoke Date"}; + } + + @Override + protected PartialList getItems(Query query) { + // Since consents are stored within profiles, we need to get all profiles and extract their consents + PartialList profiles = profileService.search(query, Profile.class); + List> consents = new ArrayList<>(); + + for (Profile profile : profiles.getList()) { + Map profileProperties = profile.getProperties(); + if (profileProperties.containsKey("consents")) { + @SuppressWarnings("unchecked") + Map profileConsents = (Map) profileProperties.get("consents"); + for (Map.Entry entry : profileConsents.entrySet()) { + Map consentMap = entry.getValue().toMap(DATE_FORMAT); + consentMap.put("profileId", profile.getItemId()); + consents.add(consentMap); + } + } + } + + return new PartialList>(consents, profiles.getOffset(), profiles.getPageSize(), profiles.getTotalSize(), PartialList.Relation.EQUAL); + } + + @Override + protected Comparable[] buildRow(Object item) { + @SuppressWarnings("unchecked") + Map consentMap = (Map) item; + return new Comparable[]{ + String.valueOf(consentMap.get("profileId")), + String.valueOf(consentMap.get("scope")), + String.valueOf(consentMap.get("typeIdentifier")), + String.valueOf(consentMap.get("status")), + String.valueOf(consentMap.get("statusDate")), + String.valueOf(consentMap.get("revokeDate")) + }; + } + + @Override + public Map read(String id) { + // The ID format is expected to be "profileId:typeIdentifier" + String[] parts = id.split(":"); + if (parts.length != 2) { + return null; + } + + String profileId = parts[0]; + String typeIdentifier = parts[1]; + + Profile profile = profileService.load(profileId); + if (profile == null) { + return null; + } + + Map profileProperties = profile.getProperties(); + if (!profileProperties.containsKey("consents")) { + return null; + } + + @SuppressWarnings("unchecked") + Map consents = (Map) profileProperties.get("consents"); + Consent consent = consents.get(typeIdentifier); + + if (consent == null) { + return null; + } + + Map consentMap = consent.toMap(DATE_FORMAT); + consentMap.put("profileId", profileId); + return consentMap; + } + + @Override + public String create(Map properties) { + String profileId = (String) properties.remove("profileId"); + if (profileId == null) { + return null; + } + + Profile profile = profileService.load(profileId); + if (profile == null) { + return null; + } + + try { + Consent consent = new Consent(properties, DATE_FORMAT); + + Map profileProperties = profile.getProperties(); + @SuppressWarnings("unchecked") + Map consents = profileProperties.containsKey("consents") ? + (Map) profileProperties.get("consents") : new HashMap<>(); + + consents.put(consent.getTypeIdentifier(), consent); + profileProperties.put("consents", consents); + profile.setProperties(profileProperties); + + profileService.save(profile); + return profileId + ":" + consent.getTypeIdentifier(); + } catch (Exception e) { + return null; + } + } + + @Override + public void update(String id, Map properties) { + // The ID format is expected to be "profileId:typeIdentifier" + String[] parts = id.split(":"); + if (parts.length != 2) { + return; + } + + String profileId = parts[0]; + String typeIdentifier = parts[1]; + + Profile profile = profileService.load(profileId); + if (profile == null) { + return; + } + + try { + Consent consent = new Consent(properties, DATE_FORMAT); + + Map profileProperties = profile.getProperties(); + @SuppressWarnings("unchecked") + Map consents = profileProperties.containsKey("consents") ? + (Map) profileProperties.get("consents") : new HashMap<>(); + + consents.put(typeIdentifier, consent); + profileProperties.put("consents", consents); + profile.setProperties(profileProperties); + + profileService.save(profile); + } catch (Exception e) { + // Handle error + } + } + + @Override + public void delete(String id) { + // The ID format is expected to be "profileId:typeIdentifier" + String[] parts = id.split(":"); + if (parts.length != 2) { + return; + } + + String profileId = parts[0]; + String typeIdentifier = parts[1]; + + Profile profile = profileService.load(profileId); + if (profile == null) { + return; + } + + Map profileProperties = profile.getProperties(); + if (profileProperties.containsKey("consents")) { + @SuppressWarnings("unchecked") + Map consents = (Map) profileProperties.get("consents"); + consents.remove(typeIdentifier); + profileProperties.put("consents", consents); + profile.setProperties(profileProperties); + profileService.save(profile); + } + } + + @Override + public List completePropertyNames(String prefix) { + return filterPropertyNames(PROPERTY_NAMES, prefix); + } + + @Override + public String getPropertiesHelp() { + return String.join("\n", + "Required properties:", + "- profileId: The identifier of the profile this consent belongs to", + "- typeIdentifier: The unique identifier for the consent type", + "- scope: The scope for this consent", + "- status: The consent status (GRANTED, DENIED, or REVOKED)", + "", + "Optional properties:", + "- statusDate: The date from which this consent applies (ISO-8601 format)", + "- revokeDate: The date at which this consent will expire (ISO-8601 format)" + ); + } +} diff --git a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/events/EventCrudCommand.java b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/events/EventCrudCommand.java new file mode 100644 index 0000000000..697867bd3d --- /dev/null +++ b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/events/EventCrudCommand.java @@ -0,0 +1,167 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.unomi.shell.dev.commands.events; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.unomi.api.Event; +import org.apache.unomi.api.PartialList; +import org.apache.unomi.api.query.Query; +import org.apache.unomi.api.services.EventService; +import org.apache.unomi.persistence.spi.CustomObjectMapper; +import org.apache.unomi.shell.dev.services.BaseCrudCommand; +import org.apache.unomi.shell.dev.services.CrudCommand; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +@Component(service = CrudCommand.class, immediate = true) +public class EventCrudCommand extends BaseCrudCommand { + + private static final ObjectMapper OBJECT_MAPPER = new CustomObjectMapper(); + private static final List PROPERTY_NAMES = List.of( + "itemId", "eventType", "scope", "source", "target", "properties", "persistent" + ); + private static final List EVENT_TYPES = List.of( + "view", "form", "login", "sessionCreated", "sessionReassigned", "profileUpdated", "incrementTrait", + "modifyConsent", "updateProperties", "identify", "impersonate", "matching" + ); + + @Reference + private EventService eventService; + + @Override + public String getObjectType() { + return "event"; + } + + @Override + protected String[] getHeadersWithoutTenant() { + return new String[] { + "Identifier", + "Event Type", + "Scope", + "Source ID", + "Target ID", + "Profile ID", + "Session ID", + "Timestamp" + }; + } + + @Override + protected String getSortBy() { + return "timeStamp:desc"; + } + + @Override + protected PartialList getItems(Query query) { + return eventService.search(query); + } + + @Override + protected Comparable[] buildRow(Object item) { + Event event = (Event) item; + ArrayList rowData = new ArrayList<>(); + rowData.add(event.getItemId()); + rowData.add(event.getEventType()); + rowData.add(event.getScope()); + rowData.add(event.getSource() != null ? event.getSource().getItemId() : ""); + rowData.add(event.getTarget() != null ? event.getTarget().getItemId() : ""); + rowData.add(event.getProfileId()); + rowData.add(event.getSessionId()); + rowData.add(event.getTimeStamp()); + return rowData.toArray(new Comparable[0]); + } + + @Override + public String create(Map properties) { + Event event = OBJECT_MAPPER.convertValue(properties, Event.class); + eventService.send(event); + return event.getItemId(); + } + + @Override + public Map read(String id) { + Event event = eventService.getEvent(id); + if (event == null) { + return null; + } + return OBJECT_MAPPER.convertValue(event, Map.class); + } + + @Override + public void update(String id, Map properties) { + properties.put("itemId", id); + Event event = OBJECT_MAPPER.convertValue(properties, Event.class); + eventService.send(event); + } + + @Override + public void delete(String id) { + eventService.deleteEvent(id); + } + + @Override + public String getPropertiesHelp() { + return String.join("\n", + "Required properties:", + "- itemId: Event ID (string)", + "- eventType: Type of event", + "- scope: Event scope", + "- source: Source object (Item)", + "- target: Target object (Item)", + "", + "Optional properties:", + "- properties: Map of event properties", + "- persistent: Whether event should be persisted (boolean)", + "", + "Common event types:", + "- view: Page/content view", + "- form: Form submission", + "- login: User login", + "- sessionCreated: New session", + "- sessionReassigned: Session reassigned", + "- profileUpdated: Profile update", + "- incrementTrait: Increment profile trait", + "- modifyConsent: Consent change", + "- updateProperties: Property update", + "- identify: Profile identification", + "- impersonate: Profile impersonation", + "- matching: Profile matching" + ); + } + + @Override + public List completePropertyNames(String prefix) { + return filterPropertyNames(PROPERTY_NAMES, prefix); + } + + @Override + public List completePropertyValue(String propertyName, String prefix) { + if ("eventType".equals(propertyName)) { + return EVENT_TYPES.stream() + .filter(type -> type.startsWith(prefix)) + .collect(Collectors.toList()); + } + return List.of(); + } + +} diff --git a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/goals/GoalCrudCommand.java b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/goals/GoalCrudCommand.java new file mode 100644 index 0000000000..38a7e77efa --- /dev/null +++ b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/goals/GoalCrudCommand.java @@ -0,0 +1,141 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.unomi.shell.dev.commands.goals; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.unomi.api.Metadata; +import org.apache.unomi.api.PartialList; +import org.apache.unomi.api.goals.Goal; +import org.apache.unomi.api.query.Query; +import org.apache.unomi.api.services.GoalsService; +import org.apache.unomi.persistence.spi.CustomObjectMapper; +import org.apache.unomi.shell.dev.services.BaseCrudCommand; +import org.apache.unomi.shell.dev.services.CrudCommand; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; + +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * A command to perform CRUD operations on goals + */ +@Component(service = CrudCommand.class, immediate = true) +public class GoalCrudCommand extends BaseCrudCommand { + + private static final ObjectMapper OBJECT_MAPPER = new CustomObjectMapper(); + private static final List PROPERTY_NAMES = List.of( + "itemId", "name", "description", "scope", "campaignId", "enabled" + ); + + @Reference + private GoalsService goalsService; + + @Override + public String getObjectType() { + return "goal"; + } + + @Override + protected String[] getHeadersWithoutTenant() { + return new String[]{"ID", "Name", "Description", "Scope", "Campaign ID", "Enabled"}; + } + + @Override + protected PartialList getItems(Query query) { + // Convert Set to PartialList for consistency + Set metadatas = goalsService.getGoalMetadatas(query); + List goals = metadatas.stream() + .map(metadata -> goalsService.getGoal(metadata.getId())) + .filter(goal -> goal != null) + .collect(Collectors.toList()); + return new PartialList<>(goals, goals.size(), 0, goals.size(), null); + } + + @Override + protected Comparable[] buildRow(Object item) { + Goal goal = (Goal) item; + return new Comparable[]{ + goal.getItemId(), + goal.getMetadata().getName(), + goal.getMetadata().getDescription(), + goal.getMetadata().getScope(), + goal.getCampaignId(), + goal.getMetadata().isEnabled() + }; + } + + @Override + public Map read(String id) { + Goal goal = goalsService.getGoal(id); + if (goal == null) { + return null; + } + return OBJECT_MAPPER.convertValue(goal, Map.class); + } + + @Override + public String create(Map properties) { + Goal goal = OBJECT_MAPPER.convertValue(properties, Goal.class); + goalsService.setGoal(goal); + return goal.getItemId(); + } + + @Override + public void update(String id, Map properties) { + Goal existingGoal = goalsService.getGoal(id); + if (existingGoal == null) { + return; + } + + Goal updatedGoal = OBJECT_MAPPER.convertValue(properties, Goal.class); + updatedGoal.setItemId(id); + goalsService.setGoal(updatedGoal); + } + + @Override + public void delete(String id) { + Goal goal = goalsService.getGoal(id); + if (goal != null) { + goalsService.removeGoal(id); + } + } + + @Override + public List completePropertyNames(String prefix) { + return filterPropertyNames(PROPERTY_NAMES, prefix); + } + + @Override + public String getPropertiesHelp() { + return String.join("\n", + "Required properties:", + "- itemId: The unique identifier of the goal", + "- name: The name of the goal", + "- description: The description of the goal", + "", + "Optional properties:", + "- scope: The scope of the goal (defaults to systemscope)", + "- campaignId: The ID of the associated campaign", + "- enabled: Whether the goal is enabled (true/false)", + "- startEvent: The condition that triggers the start of the goal", + "- targetEvent: The condition that marks the goal as achieved" + ); + } +} diff --git a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/personas/PersonaCrudCommand.java b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/personas/PersonaCrudCommand.java new file mode 100644 index 0000000000..45e0b88459 --- /dev/null +++ b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/personas/PersonaCrudCommand.java @@ -0,0 +1,150 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.unomi.shell.dev.commands.personas; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.unomi.api.PartialList; +import org.apache.unomi.api.Persona; +import org.apache.unomi.api.query.Query; +import org.apache.unomi.api.services.ProfileService; +import org.apache.unomi.persistence.spi.CustomObjectMapper; +import org.apache.unomi.shell.dev.services.BaseCrudCommand; +import org.apache.unomi.shell.dev.services.CrudCommand; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; + +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +@Component(service = CrudCommand.class, immediate = true) +public class PersonaCrudCommand extends BaseCrudCommand { + + private static final ObjectMapper OBJECT_MAPPER = new CustomObjectMapper(); + private static final List PROPERTY_NAMES = Arrays.asList( + "firstName", + "lastName", + "email", + "description", + "properties" + ); + + @Reference + private ProfileService profileService; + + @Override + public String getObjectType() { + return Persona.ITEM_TYPE; + } + + @Override + protected String[] getHeadersWithoutTenant() { + return new String[] { + "Identifier", + "First Name", + "Last Name", + "Email", + "Description", + "Last Updated" + }; + } + + @Override + protected PartialList getItems(Query query) { + return profileService.search(query, Persona.class); + } + + @Override + protected String[] buildRow(Object item) { + Persona persona = (Persona) item; + return new String[] { + persona.getItemId(), + (String) persona.getProperty("firstName"), + (String) persona.getProperty("lastName"), + (String) persona.getProperty("email"), + (String) persona.getProperty("description"), + persona.getSystemProperties().get("lastUpdated").toString() + }; + } + + @Override + public String create(Map properties) { + String personaId = (String) properties.get("itemId"); + if (personaId == null) { + throw new IllegalArgumentException("itemId is required"); + } + + Persona persona = new Persona(personaId); + for (Map.Entry entry : properties.entrySet()) { + if (!entry.getKey().equals("itemId")) { + persona.setProperty(entry.getKey(), entry.getValue()); + } + } + + Persona saved = profileService.savePersona(persona); + return saved.getItemId(); + } + + @Override + public Map read(String id) { + Persona persona = profileService.loadPersona(id); + return persona != null ? OBJECT_MAPPER.convertValue(persona, Map.class) : null; + } + + @Override + public void update(String id, Map properties) { + Persona persona = profileService.loadPersona(id); + if (persona == null) { + throw new IllegalArgumentException("Persona not found: " + id); + } + + for (Map.Entry entry : properties.entrySet()) { + persona.setProperty(entry.getKey(), entry.getValue()); + } + + profileService.savePersona(persona); + } + + @Override + public void delete(String id) { + profileService.delete(id, true); + } + + @Override + public String getPropertiesHelp() { + return "Required properties:\n" + + "- itemId: The identifier for the persona\n" + + "\n" + + "Optional properties:\n" + + "- firstName: The persona's first name\n" + + "- lastName: The persona's last name\n" + + "- email: The persona's email address\n" + + "- description: A description of what this persona represents\n" + + "- properties: Additional properties as a JSON object"; + } + + @Override + public List completePropertyNames(String prefix) { + return filterPropertyNames(PROPERTY_NAMES, prefix); + } + + @Override + public List completePropertyValue(String propertyName, String prefix) { + return List.of(); + } +} diff --git a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/profiles/ProfileAliasCrudCommand.java b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/profiles/ProfileAliasCrudCommand.java new file mode 100644 index 0000000000..64b27af140 --- /dev/null +++ b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/profiles/ProfileAliasCrudCommand.java @@ -0,0 +1,155 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.unomi.shell.dev.commands.profiles; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.unomi.api.PartialList; +import org.apache.unomi.api.ProfileAlias; +import org.apache.unomi.api.query.Query; +import org.apache.unomi.api.services.ProfileService; +import org.apache.unomi.persistence.spi.CustomObjectMapper; +import org.apache.unomi.shell.dev.services.BaseCrudCommand; +import org.apache.unomi.shell.dev.services.CrudCommand; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; + +import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * A command to perform CRUD operations on profile aliases + */ +@Component(service = CrudCommand.class, immediate = true) +public class ProfileAliasCrudCommand extends BaseCrudCommand { + + private static final ObjectMapper OBJECT_MAPPER = new CustomObjectMapper(); + private static final List PROPERTY_NAMES = List.of( + "itemId", "profileID", "clientID", "creationTime", "modifiedTime" + ); + + @Reference + private ProfileService profileService; + + @Override + public String getObjectType() { + return "profilealias"; + } + + @Override + protected String[] getHeadersWithoutTenant() { + return new String[]{"ID", "Profile ID", "Client ID", "Created", "Modified"}; + } + + @Override + protected PartialList getItems(Query query) { + // Since there's no direct method to search all aliases, we'll use findProfileAliases with null profileId + return profileService.findProfileAliases(null, query.getOffset(), query.getLimit(), query.getSortby()); + } + + @Override + protected Comparable[] buildRow(Object item) { + ProfileAlias alias = (ProfileAlias) item; + return new Comparable[]{ + alias.getItemId(), + alias.getProfileID(), + alias.getClientID(), + alias.getCreationTime() != null ? alias.getCreationTime().toString() : "", + alias.getModifiedTime() != null ? alias.getModifiedTime().toString() : "" + }; + } + + @Override + public Map read(String id) { + // Since there's no direct method to get a single alias, we'll need to search for it + // Search with a reasonable limit to find the alias by ID + PartialList aliases = profileService.findProfileAliases(null, 0, 100, null); + ProfileAlias alias = aliases.getList().stream() + .filter(a -> a.getItemId().equals(id)) + .findFirst() + .orElse(null); + + if (alias == null) { + return null; + } + return OBJECT_MAPPER.convertValue(alias, Map.class); + } + + @Override + public String create(Map properties) { + ProfileAlias alias = OBJECT_MAPPER.convertValue(properties, ProfileAlias.class); + // Set creation and modification time if not provided + if (alias.getCreationTime() == null) { + alias.setCreationTime(new Date()); + } + if (alias.getModifiedTime() == null) { + alias.setModifiedTime(alias.getCreationTime()); + } + + // Add the alias to the profile + profileService.addAliasToProfile(alias.getProfileID(), alias.getItemId(), alias.getClientID()); + return alias.getItemId(); + } + + @Override + public void update(String id, Map properties) { + // Get the existing alias (check if it exists and get its data in one call) + Map aliasData = read(id); + if (aliasData == null) { + return; + } + + // Remove the old alias and add the new one + ProfileAlias oldAlias = OBJECT_MAPPER.convertValue(aliasData, ProfileAlias.class); + profileService.removeAliasFromProfile(oldAlias.getProfileID(), oldAlias.getItemId(), oldAlias.getClientID()); + + ProfileAlias updatedAlias = OBJECT_MAPPER.convertValue(properties, ProfileAlias.class); + updatedAlias.setItemId(id); + updatedAlias.setModifiedTime(new Date()); + profileService.addAliasToProfile(updatedAlias.getProfileID(), updatedAlias.getItemId(), updatedAlias.getClientID()); + } + + @Override + public void delete(String id) { + // First check if the alias exists + Map aliasData = read(id); + if (aliasData != null) { + ProfileAlias alias = OBJECT_MAPPER.convertValue(aliasData, ProfileAlias.class); + profileService.removeAliasFromProfile(alias.getProfileID(), alias.getItemId(), alias.getClientID()); + } + } + + @Override + public List completePropertyNames(String prefix) { + return filterPropertyNames(PROPERTY_NAMES, prefix); + } + + @Override + public String getPropertiesHelp() { + return String.join("\n", + "Required properties:", + "- itemId: The unique identifier of the alias", + "- profileID: The identifier of the profile this alias belongs to", + "- clientID: The identifier of the client that created this alias", + "", + "Optional properties:", + "- creationTime: The creation timestamp (ISO-8601 format)", + "- modifiedTime: The last modification timestamp (ISO-8601 format)" + ); + } +} diff --git a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/profiles/ProfileCrudCommand.java b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/profiles/ProfileCrudCommand.java new file mode 100644 index 0000000000..3dc1c6f0c4 --- /dev/null +++ b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/profiles/ProfileCrudCommand.java @@ -0,0 +1,183 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.unomi.shell.dev.commands.profiles; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.unomi.api.PartialList; +import org.apache.unomi.api.Profile; +import org.apache.unomi.api.query.Query; +import org.apache.unomi.api.services.ProfileService; +import org.apache.unomi.persistence.spi.CustomObjectMapper; +import org.apache.unomi.shell.dev.services.BaseCrudCommand; +import org.apache.unomi.shell.dev.services.CrudCommand; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; +import org.apache.unomi.api.conditions.Condition; + +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +@Component(service = CrudCommand.class, immediate = true) +public class ProfileCrudCommand extends BaseCrudCommand { + + private static final ObjectMapper OBJECT_MAPPER = new CustomObjectMapper(); + private static final List PROPERTY_NAMES = Arrays.asList( + "itemId", + "properties.firstName", + "properties.lastName", + "properties.email", + "segments", + "scores", + "consents", + "systemProperties.lastUpdated" + ); + + private static final String[] TABLE_HEADERS = { + "Identifier", + "First Name", + "Last Name", + "Email", + "Segments", + "Last Updated", + "Scope" + }; + + @Reference + private ProfileService profileService; + + @Override + public String getObjectType() { + return "profile"; + } + + @Override + protected String[] getHeadersWithoutTenant() { + return TABLE_HEADERS; + } + + @Override + protected String getSortBy() { + return "systemProperties.lastUpdated:desc,properties.lastVisit:desc"; + } + + @Override + protected PartialList getItems(Query query) { + return profileService.search(query, Profile.class); + } + + @Override + protected Comparable[] buildRow(Object item) { + Profile profile = (Profile) item; + Map systemProperties = profile.getSystemProperties(); + return new Comparable[] { + profile.getItemId(), + (String) profile.getProperty("firstName"), + (String) profile.getProperty("lastName"), + (String) profile.getProperty("email"), + profile.getSegments().size(), + systemProperties != null ? (Comparable) systemProperties.get("lastUpdated") : null, + profile.getScope() + }; + } + + @Override + public String create(Map properties) { + Profile profile = new Profile(); + profile.setProperties(properties); + profileService.save(profile); + return profile.getItemId(); + } + + @Override + public Map read(String id) { + Profile profile = profileService.load(id); + return profile != null ? OBJECT_MAPPER.convertValue(profile, Map.class) : null; + } + + @Override + public void update(String id, Map properties) { + Profile profile = profileService.load(id); + if (profile == null) { + throw new IllegalArgumentException("Profile not found with ID: " + id); + } + profile.setProperties(properties); + profileService.save(profile); + } + + @Override + public void delete(String id) { + profileService.delete(id, false); + } + + @Override + public String getPropertiesHelp() { + return "Required properties:\n" + + " - itemId: Unique identifier for the profile\n" + + "\n" + + "Optional properties:\n" + + " - properties.firstName: First name\n" + + " - properties.lastName: Last name\n" + + " - properties.email: Email address\n" + + " - properties.phoneNumber: Phone number\n" + + " - properties.address: Address\n" + + " - properties.company: Company name\n" + + " - properties.jobTitle: Job title\n" + + " - consents: Map of consent IDs to consent status\n" + + " - scores: Map of scoring plan IDs to scores"; + } + + @Override + public List completePropertyNames(String prefix) { + return filterPropertyNames(PROPERTY_NAMES, prefix); + } + + @Override + public List completeId(String prefix) { + // Create a query with filter by ID prefix if provided + Query query = new Query(); + query.setLimit(20); // Reasonable limit for auto-completion + + try { + // If prefix is not empty, filter by it + if (!prefix.isEmpty()) { + Condition condition = new Condition(definitionsService.getConditionType("profilePropertyCondition")); + condition.setParameter("propertyName", "itemId"); + condition.setParameter("comparisonOperator", "startsWith"); + condition.setParameter("propertyValue", prefix); + query.setCondition(condition); + } else { + // Otherwise, match all + Condition matchAllCondition = new Condition(definitionsService.getConditionType("matchAllCondition")); + query.setCondition(matchAllCondition); + } + + query.setSortby("properties.lastVisit:desc"); // Sort by last visit to show most recent profiles first + + // Execute the query + PartialList profiles = profileService.search(query, Profile.class); + + // Extract IDs + return profiles.getList().stream() + .map(Profile::getItemId) + .collect(Collectors.toList()); + } catch (Exception e) { + return List.of(); // Return empty list on error + } + } +} diff --git a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/properties/PropertyTypeCrudCommand.java b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/properties/PropertyTypeCrudCommand.java new file mode 100644 index 0000000000..55b31301fb --- /dev/null +++ b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/properties/PropertyTypeCrudCommand.java @@ -0,0 +1,174 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.unomi.shell.dev.commands.properties; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.unomi.api.PartialList; +import org.apache.unomi.api.PropertyType; +import org.apache.unomi.api.query.Query; +import org.apache.unomi.api.services.ProfileService; +import org.apache.unomi.persistence.spi.CustomObjectMapper; +import org.apache.unomi.shell.dev.services.BaseCrudCommand; +import org.apache.unomi.shell.dev.services.CrudCommand; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +@Component(service = CrudCommand.class, immediate = true) +public class PropertyTypeCrudCommand extends BaseCrudCommand { + + private static final ObjectMapper OBJECT_MAPPER = new CustomObjectMapper(); + private static final List PROPERTY_NAMES = List.of( + "itemId", "name", "description", "scope", "tags", "systemTags", "target", "valueTypeId", + "defaultValue", "rank", "mergeStrategy", "multivalued", "protected" + ); + + @Reference + private ProfileService profileService; + + @Override + public String getObjectType() { + return "propertytype"; + } + + @Override + protected String[] getHeadersWithoutTenant() { + return new String[] { + "Identifier", + "Name", + "Description", + "Target", + "Value Type", + "Default Value", + "Multivalued", + "Protected", + "Rank", + "Merge Strategy", + "Scope", + "Tags", + "System Tags" + }; + } + + @Override + protected PartialList getItems(Query query) { + Map> propertyTypesByTarget = profileService.getTargetPropertyTypes(); + List propertyTypes = new ArrayList<>(); + + // Combine all property types from all targets + for (Collection typeCollection : propertyTypesByTarget.values()) { + propertyTypes.addAll(typeCollection); + } + + return paginateList(propertyTypes, query); + } + + @Override + protected Comparable[] buildRow(Object item) { + PropertyType propertyType = (PropertyType) item; + return new Comparable[] { + propertyType.getItemId(), + propertyType.getMetadata().getName(), + propertyType.getMetadata().getDescription(), + propertyType.getTarget(), + propertyType.getValueTypeId(), + propertyType.getDefaultValue(), + String.valueOf(propertyType.isMultivalued()), + String.valueOf(propertyType.isProtected()), + propertyType.getRank() != null ? propertyType.getRank().toString() : "", + propertyType.getMergeStrategy(), + propertyType.getMetadata().getScope(), + String.join(",", propertyType.getMetadata().getTags()), + String.join(",", propertyType.getMetadata().getSystemTags()) + }; + } + + @Override + public String create(Map properties) { + PropertyType propertyType = OBJECT_MAPPER.convertValue(properties, PropertyType.class); + profileService.setPropertyType(propertyType); + return propertyType.getItemId(); + } + + @Override + public Map read(String id) { + PropertyType propertyType = profileService.getPropertyType(id); + if (propertyType != null) { + return OBJECT_MAPPER.convertValue(propertyType, Map.class); + } + return null; + } + + @Override + public void update(String id, Map properties) { + PropertyType propertyType = OBJECT_MAPPER.convertValue(properties, PropertyType.class); + propertyType.setItemId(id); + profileService.setPropertyType(propertyType); + } + + @Override + public void delete(String id) { + profileService.deletePropertyType(id); + } + + @Override + public String getPropertiesHelp() { + return String.join("\n", + "Required properties:", + "- itemId: Identifier for the property type", + "- name: Name of the property type", + "- valueTypeId: Type of value (string, integer, long, date, etc.)", + "", + "Optional properties:", + "- description: Description of the property type", + "- target: Target type (e.g., 'profiles')", + "- defaultValue: Default value for the property", + "- rank: Numeric rank for ordering", + "- mergeStrategy: Strategy for merging property values", + "- multivalued: Whether the property can have multiple values (true/false)", + "- protected: Whether the property is read-only (true/false)", + "- scope: Scope of the property type", + "- tags: List of tags", + "- systemTags: List of system tags" + ); + } + + @Override + public List completePropertyNames(String prefix) { + return filterPropertyNames(PROPERTY_NAMES, prefix); + } + + @Override + public List completePropertyValue(String propertyName, String prefix) { + if ("valueTypeId".equals(propertyName)) { + return List.of("string", "integer", "long", "date", "boolean", "json") + .stream() + .filter(type -> type.startsWith(prefix)) + .collect(Collectors.toList()); + } + return List.of(); + } + + public void setProfileService(ProfileService profileService) { + this.profileService = profileService; + } +} diff --git a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/rules/RuleCrudCommand.java b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/rules/RuleCrudCommand.java new file mode 100644 index 0000000000..64d523926f --- /dev/null +++ b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/rules/RuleCrudCommand.java @@ -0,0 +1,209 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.unomi.shell.dev.commands.rules; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.commons.lang3.StringUtils; +import org.apache.unomi.api.Metadata; +import org.apache.unomi.api.PartialList; +import org.apache.unomi.api.query.Query; +import org.apache.unomi.api.rules.Rule; +import org.apache.unomi.api.rules.RuleStatistics; +import org.apache.unomi.api.services.RulesService; +import org.apache.unomi.persistence.spi.CustomObjectMapper; +import org.apache.unomi.shell.dev.services.BaseCrudCommand; +import org.apache.unomi.shell.dev.services.CrudCommand; +import org.apache.unomi.api.conditions.Condition; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +@Component(service = CrudCommand.class, immediate = true) +public class RuleCrudCommand extends BaseCrudCommand { + + private static final ObjectMapper OBJECT_MAPPER = new CustomObjectMapper(); + private static final List PROPERTY_NAMES = List.of( + "itemId", "name", "description", "priority", "condition", "actions", "metadata" + ); + private static final List CONDITION_TYPES = List.of( + "booleanCondition", "profilePropertyCondition", "sessionPropertyCondition", "eventPropertyCondition", + "pastEventCondition", "matchAllCondition", "notCondition", "orCondition", "andCondition" + ); + + @Reference + private RulesService rulesService; + + @Override + public String getObjectType() { + return "rule"; + } + + @Override + protected String[] getHeadersWithoutTenant() { + return new String[] { + "Activated", + "Hidden", + "Read-only", + "Identifier", + "Scope", + "Name", + "Tags", + "System tags", + "Executions", + "Conditions [ms]", + "Actions [ms]" + }; + } + + @Override + protected PartialList getItems(Query query) { + return rulesService.getRuleDetails(query); + } + + @Override + protected Comparable[] buildRow(Object item) { + Rule rule = (Rule) item; + String ruleId = rule.getItemId(); + Map allRuleStatistics = rulesService.getAllRuleStatistics(); + + ArrayList rowData = new ArrayList<>(); + rowData.add(rule.getMetadata().isEnabled() ? "x" : ""); + rowData.add(rule.getMetadata().isHidden() ? "x" : ""); + rowData.add(rule.getMetadata().isReadOnly() ? "x" : ""); + rowData.add(ruleId); + rowData.add(rule.getMetadata().getScope()); + rowData.add(rule.getMetadata().getName()); + rowData.add(StringUtils.join(rule.getMetadata().getTags(), ",")); + rowData.add(StringUtils.join(rule.getMetadata().getSystemTags(), ",")); + RuleStatistics ruleStatistics = allRuleStatistics.get(ruleId); + if (ruleStatistics != null) { + rowData.add(ruleStatistics.getExecutionCount()); + rowData.add(ruleStatistics.getConditionsTime()); + rowData.add(ruleStatistics.getActionsTime()); + } else { + rowData.add(0L); + rowData.add(0L); + rowData.add(0L); + } + return rowData.toArray(new Comparable[0]); + } + + @Override + public String create(Map properties) { + Rule rule = OBJECT_MAPPER.convertValue(properties, Rule.class); + rulesService.setRule(rule); + return rule.getItemId(); + } + + @Override + public Map read(String id) { + Rule rule = rulesService.getRule(id); + if (rule == null) { + return null; + } + return OBJECT_MAPPER.convertValue(rule, Map.class); + } + + @Override + public void update(String id, Map properties) { + properties.put("itemId", id); + Rule rule = OBJECT_MAPPER.convertValue(properties, Rule.class); + rulesService.setRule(rule); + } + + @Override + public void delete(String id) { + rulesService.removeRule(id); + } + + @Override + public String getPropertiesHelp() { + return String.join("\n", + "Required properties:", + "- itemId: Rule ID (string)", + "- name: Rule name", + "- condition: Rule condition object", + "- actions: List of rule actions", + "", + "Optional properties:", + "- description: Rule description", + "- priority: Rule priority (integer)", + "- metadata: Rule metadata", + "", + "Condition types:", + "- booleanCondition: Simple true/false condition", + "- profilePropertyCondition: Match profile property", + "- sessionPropertyCondition: Match session property", + "- eventPropertyCondition: Match event property", + "- pastEventCondition: Match past events", + "- matchAllCondition: Match all sub-conditions", + "- notCondition: Negate sub-condition", + "- orCondition: Match any sub-condition", + "- andCondition: Match all sub-conditions" + ); + } + + @Override + public List completePropertyNames(String prefix) { + return filterPropertyNames(PROPERTY_NAMES, prefix); + } + + @Override + public List completePropertyValue(String propertyName, String prefix) { + if ("condition.type".equals(propertyName)) { + return CONDITION_TYPES.stream() + .filter(type -> type.startsWith(prefix)) + .collect(Collectors.toList()); + } + return List.of(); + } + + @Override + public List completeId(String prefix) { + // Create a query to find rules that match the prefix + Query query = new Query(); + query.setLimit(20); // Reasonable limit for auto-completion + + // If prefix is not empty, use it to filter results + if (!prefix.isEmpty()) { + Condition condition = new Condition(definitionsService.getConditionType("sessionPropertyCondition")); + condition.setParameter("comparisonOperator", "startsWith"); + condition.setParameter("propertyName", "itemId"); + condition.setParameter("propertyValue", prefix); + query.setCondition(condition); + } else { + // Otherwise, match all + Condition matchAllCondition = new Condition(definitionsService.getConditionType("matchAllCondition")); + query.setCondition(matchAllCondition); + } + + // Execute the query and extract rule IDs + try { + PartialList metadatas = rulesService.getRuleMetadatas(query); + return metadatas.getList().stream() + .map(Metadata::getId) + .collect(Collectors.toList()); + } catch (Exception e) { + return List.of(); // Return empty list on error + } + } + +} diff --git a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/rules/RuleStatisticsCrudCommand.java b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/rules/RuleStatisticsCrudCommand.java new file mode 100644 index 0000000000..8a0507bdd5 --- /dev/null +++ b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/rules/RuleStatisticsCrudCommand.java @@ -0,0 +1,135 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.unomi.shell.dev.commands.rules; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.unomi.api.PartialList; +import org.apache.unomi.api.query.Query; +import org.apache.unomi.api.rules.RuleStatistics; +import org.apache.unomi.api.services.RulesService; +import org.apache.unomi.persistence.spi.CustomObjectMapper; +import org.apache.unomi.shell.dev.services.BaseCrudCommand; +import org.apache.unomi.shell.dev.services.CrudCommand; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * A command to perform CRUD operations on rule statistics + */ +@Component(service = CrudCommand.class, immediate = true) +public class RuleStatisticsCrudCommand extends BaseCrudCommand { + + private static final ObjectMapper OBJECT_MAPPER = new CustomObjectMapper(); + private static final List PROPERTY_NAMES = List.of( + "itemId", "executionCount", "localExecutionCount", "conditionsTime", "localConditionsTime", "actionsTime", "localActionsTime", "lastSyncDate" + ); + + @Reference + private RulesService rulesService; + + @Override + public String getObjectType() { + return "rulestats"; + } + + @Override + protected String[] getHeadersWithoutTenant() { + return new String[]{"ID", "Executions", "Local Executions", "Conditions Time", "Local Conditions Time", "Actions Time", "Local Actions Time", "Last Sync"}; + } + + @Override + protected PartialList getItems(Query query) { + // Get all rules and their statistics + Map statisticsMap = rulesService.getAllRuleStatistics(); + List statistics = new ArrayList<>(statisticsMap.values()); + return new PartialList<>(statistics, 0, statistics.size(), statistics.size(), PartialList.Relation.EQUAL); + } + + @Override + protected Comparable[] buildRow(Object item) { + RuleStatistics stats = (RuleStatistics) item; + return new Comparable[]{ + stats.getItemId(), + stats.getExecutionCount(), + stats.getLocalExecutionCount(), + stats.getConditionsTime(), + stats.getLocalConditionsTime(), + stats.getActionsTime(), + stats.getLocalActionsTime(), + stats.getLastSyncDate() != null ? stats.getLastSyncDate().toString() : "" + }; + } + + @Override + public Map read(String id) { + RuleStatistics stats = rulesService.getRuleStatistics(id); + if (stats == null) { + return null; + } + return OBJECT_MAPPER.convertValue(stats, Map.class); + } + + @Override + public String create(Map properties) { + // Note: RulesService doesn't provide a direct way to create rule statistics + // They are automatically managed by the rules engine + return null; + } + + @Override + public void update(String id, Map properties) { + // Note: RulesService doesn't provide a direct way to update rule statistics + // They are automatically managed by the rules engine + } + + @Override + public void delete(String id) { + // Note: RulesService doesn't provide a direct way to delete individual rule statistics + // They are automatically managed by the rules engine + // You can use resetAllRuleStatistics() to reset all statistics to zero + rulesService.resetAllRuleStatistics(); + } + + @Override + public List completePropertyNames(String prefix) { + return filterPropertyNames(PROPERTY_NAMES, prefix); + } + + @Override + public String getPropertiesHelp() { + return String.join("\n", + "Rule statistics are automatically managed by the rules engine and cannot be directly modified.", + "You can view statistics using the following properties:", + "", + "- itemId: The unique identifier of the rule statistics (matches the rule ID)", + "- executionCount: Total number of rule executions in the cluster", + "- localExecutionCount: Number of rule executions on this node since last sync", + "- conditionsTime: Total time spent evaluating conditions in the cluster (ms)", + "- localConditionsTime: Time spent evaluating conditions on this node since last sync (ms)", + "- actionsTime: Total time spent executing actions in the cluster (ms)", + "- localActionsTime: Time spent executing actions on this node since last sync (ms)", + "- lastSyncDate: Date of the last synchronization with the cluster", + "", + "Note: Use 'unomi:crud rulestats reset' to reset all rule statistics to zero." + ); + } +} diff --git a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/scopes/ScopeCrudCommand.java b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/scopes/ScopeCrudCommand.java new file mode 100644 index 0000000000..10bef1fca0 --- /dev/null +++ b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/scopes/ScopeCrudCommand.java @@ -0,0 +1,139 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.unomi.shell.dev.commands.scopes; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.commons.lang3.StringUtils; +import org.apache.unomi.api.PartialList; +import org.apache.unomi.api.Scope; +import org.apache.unomi.api.query.Query; +import org.apache.unomi.api.services.ScopeService; +import org.apache.unomi.persistence.spi.CustomObjectMapper; +import org.apache.unomi.persistence.spi.PersistenceService; +import org.apache.unomi.shell.dev.services.BaseCrudCommand; +import org.apache.unomi.shell.dev.services.CrudCommand; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +@Component(service = CrudCommand.class, immediate = true) +public class ScopeCrudCommand extends BaseCrudCommand { + + private static final ObjectMapper OBJECT_MAPPER = new CustomObjectMapper(); + private static final List PROPERTY_NAMES = List.of( + "itemId", "name", "description", "restrictedVisibility", "metadata" + ); + + @Reference + private ScopeService scopeService; + + @Reference + private PersistenceService persistenceService; + + @Override + public String getObjectType() { + return "scope"; + } + + @Override + protected String[] getHeadersWithoutTenant() { + return new String[] { + "Activated", + "Hidden", + "Read-only", + "Identifier", + "Name", + "Description", + "Restricted", + "Tags", + "System tags" + }; + } + + @Override + protected PartialList getItems(Query query) { + return persistenceService.query(query.getCondition(), query.getSortby(), Scope.class, query.getOffset(), query.getLimit()); + } + + @Override + protected Comparable[] buildRow(Object item) { + Scope scope = (Scope) item; + ArrayList rowData = new ArrayList<>(); + rowData.add(scope.getMetadata().isEnabled() ? "x" : ""); + rowData.add(scope.getMetadata().isHidden() ? "x" : ""); + rowData.add(scope.getMetadata().isReadOnly() ? "x" : ""); + rowData.add(scope.getItemId()); + rowData.add(scope.getMetadata().getName()); + rowData.add(scope.getMetadata().getDescription()); + rowData.add(""); // No restricted visibility in Scope class + rowData.add(StringUtils.join(scope.getMetadata().getTags(), ",")); + rowData.add(StringUtils.join(scope.getMetadata().getSystemTags(), ",")); + return rowData.toArray(new Comparable[0]); + } + + @Override + public String create(Map properties) { + Scope scope = OBJECT_MAPPER.convertValue(properties, Scope.class); + scopeService.save(scope); + return scope.getItemId(); + } + + @Override + public Map read(String id) { + Scope scope = scopeService.getScope(id); + if (scope == null) { + return null; + } + return OBJECT_MAPPER.convertValue(scope, Map.class); + } + + @Override + public void update(String id, Map properties) { + properties.put("itemId", id); + Scope scope = OBJECT_MAPPER.convertValue(properties, Scope.class); + scopeService.save(scope); + } + + @Override + public void delete(String id) { + scopeService.delete(id); + } + + @Override + public String getPropertiesHelp() { + return String.join("\n", + "Required properties:", + "- itemId: Scope ID (string)", + "- name: Scope name", + "", + "Optional properties:", + "- description: Scope description", + "- restrictedVisibility: Whether scope has restricted visibility (boolean)", + "- metadata: Scope metadata" + ); + } + + @Override + public List completePropertyNames(String prefix) { + return filterPropertyNames(PROPERTY_NAMES, prefix); + } + +} diff --git a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/scoring/ScoringCrudCommand.java b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/scoring/ScoringCrudCommand.java new file mode 100644 index 0000000000..78fd511dd3 --- /dev/null +++ b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/scoring/ScoringCrudCommand.java @@ -0,0 +1,142 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.unomi.shell.dev.commands.scoring; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.commons.lang3.StringUtils; +import org.apache.unomi.api.Metadata; +import org.apache.unomi.api.PartialList; +import org.apache.unomi.api.query.Query; +import org.apache.unomi.api.segments.Scoring; +import org.apache.unomi.api.services.SegmentService; +import org.apache.unomi.persistence.spi.CustomObjectMapper; +import org.apache.unomi.shell.dev.services.BaseCrudCommand; +import org.apache.unomi.shell.dev.services.CrudCommand; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +@Component(service = CrudCommand.class, immediate = true) +public class ScoringCrudCommand extends BaseCrudCommand { + + private static final ObjectMapper OBJECT_MAPPER = new CustomObjectMapper(); + private static final List PROPERTY_NAMES = List.of( + "itemId", "scope", "name", "description", "elements", "metadata" + ); + + @Reference + private SegmentService segmentService; + + @Override + public String getObjectType() { + return "scoring"; + } + + @Override + protected String[] getHeadersWithoutTenant() { + return new String[] { + "Activated", + "Hidden", + "Read-only", + "Identifier", + "Scope", + "Name", + "Tags", + "System tags" + }; + } + + @Override + protected PartialList getItems(Query query) { + return segmentService.getScoringMetadatas(query); + } + + @Override + protected Comparable[] buildRow(Object item) { + Metadata metadata = (Metadata) item; + ArrayList rowData = new ArrayList<>(); + rowData.add(metadata.isEnabled() ? "x" : ""); + rowData.add(metadata.isHidden() ? "x" : ""); + rowData.add(metadata.isReadOnly() ? "x" : ""); + rowData.add(metadata.getId()); + rowData.add(metadata.getScope()); + rowData.add(metadata.getName()); + rowData.add(StringUtils.join(metadata.getTags(), ",")); + rowData.add(StringUtils.join(metadata.getSystemTags(), ",")); + return rowData.toArray(new Comparable[0]); + } + + @Override + public String create(Map properties) { + Scoring scoring = OBJECT_MAPPER.convertValue(properties, Scoring.class); + segmentService.setScoringDefinition(scoring); + return scoring.getItemId(); + } + + @Override + public Map read(String id) { + Scoring scoring = segmentService.getScoringDefinition(id); + if (scoring == null) { + return null; + } + return OBJECT_MAPPER.convertValue(scoring, Map.class); + } + + @Override + public void update(String id, Map properties) { + Scoring scoring = segmentService.getScoringDefinition(id); + if (scoring == null) { + throw new IllegalArgumentException("Scoring with id '" + id + "' not found"); + } + Scoring updatedScoring = OBJECT_MAPPER.convertValue(properties, Scoring.class); + updatedScoring.setItemId(id); + segmentService.setScoringDefinition(updatedScoring); + } + + @Override + public void delete(String id) { + segmentService.removeScoringDefinition(id, false); + } + + @Override + public List completePropertyNames(String prefix) { + return filterPropertyNames(PROPERTY_NAMES, prefix); + } + + @Override + public String getPropertiesHelp() { + return "Required properties:\n" + + "- itemId (string): Unique identifier for the scoring\n" + + "- scope (string): Scope of the scoring\n" + + "- name (string): Human-readable name\n" + + "\n" + + "Optional properties:\n" + + "- description (string): Description of the scoring\n" + + "- elements (array): List of scoring elements, each containing:\n" + + " - condition (object): Condition that triggers the scoring element\n" + + " - value (number): Score value to add when condition is met\n" + + "- enabled (boolean): Whether the scoring is enabled (default: true)\n" + + "- hidden (boolean): Whether the scoring is hidden (default: false)\n" + + "- readOnly (boolean): Whether the scoring is read-only (default: false)\n" + + "- tags (array): List of tags for the scoring\n" + + "- systemTags (array): List of system tags for the scoring"; + } +} diff --git a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/segments/SegmentCrudCommand.java b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/segments/SegmentCrudCommand.java new file mode 100644 index 0000000000..85e16aecc7 --- /dev/null +++ b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/segments/SegmentCrudCommand.java @@ -0,0 +1,195 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.unomi.shell.dev.commands.segments; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.commons.lang3.StringUtils; +import org.apache.unomi.api.Metadata; +import org.apache.unomi.api.PartialList; +import org.apache.unomi.api.query.Query; +import org.apache.unomi.api.segments.Segment; +import org.apache.unomi.api.services.SegmentService; +import org.apache.unomi.persistence.spi.CustomObjectMapper; +import org.apache.unomi.shell.dev.services.BaseCrudCommand; +import org.apache.unomi.shell.dev.services.CrudCommand; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; +import org.apache.unomi.api.conditions.Condition; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +@Component(service = CrudCommand.class, immediate = true) +public class SegmentCrudCommand extends BaseCrudCommand { + + private static final ObjectMapper OBJECT_MAPPER = new CustomObjectMapper(); + private static final List PROPERTY_NAMES = List.of( + "itemId", "name", "description", "scope", "condition", "metadata" + ); + private static final List CONDITION_TYPES = List.of( + "booleanCondition", "profilePropertyCondition", "sessionPropertyCondition", "eventPropertyCondition", + "pastEventCondition", "matchAllCondition", "notCondition", "orCondition", "andCondition", + "profileSegmentCondition", "scoringCondition" + ); + + @Reference + private SegmentService segmentService; + + @Override + public String getObjectType() { + return "segment"; + } + + @Override + protected String[] getHeadersWithoutTenant() { + return new String[] { + "Activated", + "Hidden", + "Read-only", + "Identifier", + "Scope", + "Name", + "Tags", + "System tags" + }; + } + + @Override + protected PartialList getItems(Query query) { + return segmentService.getSegmentMetadatas(query); + } + + @Override + protected Comparable[] buildRow(Object item) { + Metadata segmentMetadata = (Metadata) item; + ArrayList rowData = new ArrayList<>(); + rowData.add(segmentMetadata.isEnabled() ? "x" : ""); + rowData.add(segmentMetadata.isHidden() ? "x" : ""); + rowData.add(segmentMetadata.isReadOnly() ? "x" : ""); + rowData.add(segmentMetadata.getId()); + rowData.add(segmentMetadata.getScope()); + rowData.add(segmentMetadata.getName()); + rowData.add(StringUtils.join(segmentMetadata.getTags(), ",")); + rowData.add(StringUtils.join(segmentMetadata.getSystemTags(), ",")); + return rowData.toArray(new Comparable[0]); + } + + @Override + public String create(Map properties) { + Segment segment = OBJECT_MAPPER.convertValue(properties, Segment.class); + segmentService.setSegmentDefinition(segment); + return segment.getItemId(); + } + + @Override + public Map read(String id) { + Segment segment = segmentService.getSegmentDefinition(id); + if (segment == null) { + return null; + } + return OBJECT_MAPPER.convertValue(segment, Map.class); + } + + @Override + public void update(String id, Map properties) { + properties.put("itemId", id); + Segment segment = OBJECT_MAPPER.convertValue(properties, Segment.class); + segmentService.setSegmentDefinition(segment); + } + + @Override + public void delete(String id) { + segmentService.removeSegmentDefinition(id, false); + } + + @Override + public String getPropertiesHelp() { + return String.join("\n", + "Required properties:", + "- itemId: Segment ID (string)", + "- name: Segment name", + "- condition: Segment condition object", + "", + "Optional properties:", + "- description: Segment description", + "- scope: Segment scope", + "- metadata: Segment metadata", + "", + "Condition types:", + "- booleanCondition: Simple true/false condition", + "- profilePropertyCondition: Match profile property", + "- sessionPropertyCondition: Match session property", + "- eventPropertyCondition: Match event property", + "- pastEventCondition: Match past events", + "- matchAllCondition: Match all sub-conditions", + "- notCondition: Negate sub-condition", + "- orCondition: Match any sub-condition", + "- andCondition: Match all sub-conditions", + "- profileSegmentCondition: Match profile segment", + "- scoringCondition: Match scoring value" + ); + } + + @Override + public List completePropertyNames(String prefix) { + return filterPropertyNames(PROPERTY_NAMES, prefix); + } + + @Override + public List completePropertyValue(String propertyName, String prefix) { + if ("condition.type".equals(propertyName)) { + return CONDITION_TYPES.stream() + .filter(type -> type.startsWith(prefix)) + .collect(Collectors.toList()); + } else if ("scope".equals(propertyName)) { + return List.of(); + } + return List.of(); + } + + @Override + public List completeId(String prefix) { + // Create a query to find segments that match the prefix + Query query = new Query(); + query.setLimit(20); // Reasonable limit for auto-completion + + try { + // If prefix is not empty, use it to filter results + if (!prefix.isEmpty()) { + Condition condition = new Condition(definitionsService.getConditionType("booleanCondition")); + condition.setParameter("operator", "startsWith"); + condition.setParameter("propertyName", "itemId"); + condition.setParameter("propertyValue", prefix); + query.setCondition(condition); + } else { + // Otherwise, match all + Condition matchAllCondition = new Condition(definitionsService.getConditionType("matchAllCondition")); + query.setCondition(matchAllCondition); + } + + // Execute the query and extract segment IDs + PartialList metadatas = segmentService.getSegmentMetadatas(query); + return metadatas.getList().stream() + .map(Metadata::getId) + .collect(Collectors.toList()); + } catch (Exception e) { + return List.of(); // Return empty list on error + } + } +} diff --git a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/sessions/SessionCrudCommand.java b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/sessions/SessionCrudCommand.java new file mode 100644 index 0000000000..a185163d1a --- /dev/null +++ b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/sessions/SessionCrudCommand.java @@ -0,0 +1,129 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.unomi.shell.dev.commands.sessions; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.unomi.api.PartialList; +import org.apache.unomi.api.Session; +import org.apache.unomi.api.query.Query; +import org.apache.unomi.api.services.ProfileService; +import org.apache.unomi.persistence.spi.CustomObjectMapper; +import org.apache.unomi.shell.dev.services.BaseCrudCommand; +import org.apache.unomi.shell.dev.services.CrudCommand; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; + +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * A command to perform CRUD operations on sessions + */ +@Component(service = CrudCommand.class, immediate = true) +public class SessionCrudCommand extends BaseCrudCommand { + + private static final ObjectMapper OBJECT_MAPPER = new CustomObjectMapper(); + private static final List PROPERTY_NAMES = List.of( + "itemId", "profileId", "properties", "systemProperties", "timeStamp", "scope", "lastEventDate", "size", "duration", "originEventTypes", "originEventIds" + ); + + @Reference + private ProfileService profileService; + + @Override + public String getObjectType() { + return "session"; + } + + @Override + protected String[] getHeadersWithoutTenant() { + return new String[]{"ID", "Profile ID", "Scope", "Time Stamp", "Last Event", "Duration", "Size"}; + } + + @Override + protected PartialList getItems(Query query) { + return profileService.searchSessions(query); + } + + @Override + protected Comparable[] buildRow(Object item) { + Session session = (Session) item; + return new Comparable[]{ + session.getItemId(), + session.getProfileId(), + session.getScope(), + session.getTimeStamp() != null ? session.getTimeStamp().toString() : "", + session.getLastEventDate() != null ? session.getLastEventDate().toString() : "", + String.valueOf(session.getDuration()), + String.valueOf(session.getSize()) + }; + } + + @Override + public Map read(String id) { + Session session = profileService.loadSession(id, null); + if (session == null) { + return null; + } + return OBJECT_MAPPER.convertValue(session, Map.class); + } + + @Override + public String create(Map properties) { + Session session = OBJECT_MAPPER.convertValue(properties, Session.class); + profileService.saveSession(session); + return session.getItemId(); + } + + @Override + public void update(String id, Map properties) { + Session updatedSession = OBJECT_MAPPER.convertValue(properties, Session.class); + updatedSession.setItemId(id); + profileService.saveSession(updatedSession); + } + + @Override + public void delete(String id) { + profileService.deleteSession(id); + } + + @Override + public List completePropertyNames(String prefix) { + return filterPropertyNames(PROPERTY_NAMES, prefix); + } + + @Override + public String getPropertiesHelp() { + return String.join("\n", + "Required properties:", + "- itemId: The unique identifier of the session", + "- profileId: The identifier of the associated profile", + "- timeStamp: The session creation timestamp (ISO-8601 format)", + "- scope: The scope of the session", + "", + "Optional properties:", + "- properties: A map of custom properties for the session", + "- systemProperties: A map of system properties for internal use", + "- lastEventDate: The date of the last event in the session (ISO-8601 format)", + "- size: The size of the session", + "- duration: The duration of the session in milliseconds", + "- originEventTypes: List of event types that caused the session creation", + "- originEventIds: List of event IDs that caused the session creation" + ); + } +} diff --git a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/topics/TopicCrudCommand.java b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/topics/TopicCrudCommand.java new file mode 100644 index 0000000000..f97bed28d9 --- /dev/null +++ b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/topics/TopicCrudCommand.java @@ -0,0 +1,120 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.unomi.shell.dev.commands.topics; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.unomi.api.PartialList; +import org.apache.unomi.api.Topic; +import org.apache.unomi.api.query.Query; +import org.apache.unomi.api.services.TopicService; +import org.apache.unomi.persistence.spi.CustomObjectMapper; +import org.apache.unomi.shell.dev.services.BaseCrudCommand; +import org.apache.unomi.shell.dev.services.CrudCommand; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; + +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +@Component(service = CrudCommand.class, immediate = true) +public class TopicCrudCommand extends BaseCrudCommand { + + private static final ObjectMapper OBJECT_MAPPER = new CustomObjectMapper(); + private static final List PROPERTY_NAMES = List.of( + "itemId", "name", "description", "parentId", "metadata" + ); + + @Reference + private TopicService topicService; + + @Override + public String getObjectType() { + return "topic"; + } + + @Override + protected String[] getHeadersWithoutTenant() { + return new String[] { + "Identifier", + "Name", + "Scope" + }; + } + + @Override + protected PartialList getItems(Query query) { + return topicService.search(query); + } + + @Override + protected String[] buildRow(Object item) { + Topic topic = (Topic) item; + return new String[] { + topic.getItemId(), + topic.getName(), + topic.getScope() + }; + } + + @Override + public String create(Map properties) { + Topic topic = OBJECT_MAPPER.convertValue(properties, Topic.class); + topicService.save(topic); + return topic.getItemId(); + } + + @Override + public Map read(String id) { + Topic topic = topicService.load(id); + if (topic == null) { + return null; + } + return OBJECT_MAPPER.convertValue(topic, Map.class); + } + + @Override + public void update(String id, Map properties) { + properties.put("itemId", id); + Topic topic = OBJECT_MAPPER.convertValue(properties, Topic.class); + topicService.save(topic); + } + + @Override + public void delete(String id) { + topicService.delete(id); + } + + @Override + public String getPropertiesHelp() { + return String.join("\n", + "Required properties:", + "- itemId: Topic ID (string)", + "- name: Topic name", + "", + "Optional properties:", + "- description: Topic description", + "- parentId: Parent topic ID", + "- metadata: Topic metadata" + ); + } + + @Override + public List completePropertyNames(String prefix) { + return filterPropertyNames(PROPERTY_NAMES, prefix); + } +} diff --git a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/completers/BaseCompleter.java b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/completers/BaseCompleter.java new file mode 100644 index 0000000000..5581f519bd --- /dev/null +++ b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/completers/BaseCompleter.java @@ -0,0 +1,86 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.unomi.shell.dev.completers; + +import org.apache.karaf.shell.api.action.lifecycle.Reference; +import org.apache.karaf.shell.api.console.CommandLine; +import org.apache.karaf.shell.api.console.Completer; +import org.apache.karaf.shell.api.console.Session; +import org.apache.karaf.shell.support.completers.StringsCompleter; +import org.apache.unomi.api.Item; +import org.apache.unomi.api.PartialList; +import org.apache.unomi.api.conditions.Condition; +import org.apache.unomi.api.query.Query; +import org.apache.unomi.api.services.DefinitionsService; +import org.apache.unomi.persistence.spi.PersistenceService; + +import java.io.PrintStream; +import java.util.List; + +/** + * Base class for completers + */ +public abstract class BaseCompleter implements Completer { + protected static final int DEFAULT_LIMIT = 50; + + @Reference + protected PersistenceService persistenceService; + + @Reference + protected DefinitionsService definitionsService; + + protected abstract Class getItemType(); + + protected abstract String getSortBy(); + + @Override + public int complete(Session session, CommandLine commandLine, List candidates) { + StringsCompleter delegate = new StringsCompleter(); + + try { + Query query = new Query(); + query.setLimit(DEFAULT_LIMIT); + query.setSortby(getSortBy()); + + Condition condition = new Condition(); + condition.setConditionType(definitionsService.getConditionType("matchAllCondition")); + query.setCondition(condition); + + PartialList items = persistenceService.query(query.getCondition(), query.getSortby(), getItemType(), query.getOffset(), query.getLimit()); + + for (T item : items.getList()) { + delegate.getStrings().add(item.getItemId()); + } + + return delegate.complete(session, commandLine, candidates); + } catch (Exception e) { + // Log error but don't fail completion + // Note: Printing during completion can interfere with completion, but using console for consistency + PrintStream console = session.getConsole(); + console.println("Error: Error completing items: " + e.getMessage()); + return -1; + } + } + + public void setPersistenceService(PersistenceService persistenceService) { + this.persistenceService = persistenceService; + } + + public void setDefinitionsService(DefinitionsService definitionsService) { + this.definitionsService = definitionsService; + } +} diff --git a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/completers/IdCompleter.java b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/completers/IdCompleter.java new file mode 100644 index 0000000000..71f6df4123 --- /dev/null +++ b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/completers/IdCompleter.java @@ -0,0 +1,156 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.unomi.shell.dev.completers; + +import org.apache.karaf.shell.api.action.lifecycle.Init; +import org.apache.karaf.shell.api.action.lifecycle.Reference; +import org.apache.karaf.shell.api.action.lifecycle.Service; +import org.apache.karaf.shell.api.console.CommandLine; +import org.apache.karaf.shell.api.console.Completer; +import org.apache.karaf.shell.api.console.Session; +import org.apache.karaf.shell.support.completers.StringsCompleter; +import org.apache.unomi.shell.dev.services.CrudCommand; +import org.osgi.framework.BundleContext; +import org.osgi.framework.ServiceReference; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.List; + +@Service +public class IdCompleter implements Completer { + + private static final Logger LOGGER = LoggerFactory.getLogger(IdCompleter.class.getName()); + + @Reference + private BundleContext bundleContext; + + @Init + public void init() { + LOGGER.debug("IdCompleter initialized"); + } + + @Override + public int complete(Session session, CommandLine commandLine, List candidates) { + // Get the operation and type from the command line + // args[0] = "crud" (command name), args[1] = operation, args[2] = type, args[3+] = remaining + String operation = null; + String type = null; + String[] args = commandLine.getArguments(); + if (args.length > 1) { + operation = args[1]; + } + if (args.length > 2) { + type = args[2]; + } + if (type == null) { + return -1; + } + + // Determine if ID completion is appropriate based on operation + // ID completion is only appropriate for: read, delete, and update (first argument) + if (operation != null) { + String operationLower = operation.toLowerCase(); + if (!"read".equals(operationLower) && !"delete".equals(operationLower) && !"update".equals(operationLower)) { + // For create, list, help - ID completion is not appropriate + return -1; + } + } + + // Determine which argument we're completing based on args.length + // args[0] = "crud" (command name) + // args[1] = operation + // args[2] = type + // args[3+] = remaining (multi-valued) + // For read/delete: remaining[0] = ID (complete when args.length == 3, i.e., we're at remaining[0]) + // For update: remaining[0] = ID, remaining[1] = JSON + // - Complete IDs when args.length == 3 (completing remaining[0], which is the ID) + // - Don't complete IDs when args.length >= 4 (completing remaining[1], which is JSON) + + // For update operation, if args.length >= 4, we're past the ID argument + // and are completing the JSON part, so don't complete IDs + if (operation != null && "update".equals(operation.toLowerCase()) && args.length >= 4) { + // We're past the ID argument, so we're completing JSON - don't complete IDs + return -1; + } + + // For read/delete/update with args.length == 3, we're completing the ID (remaining[0]) + // The completer is attached to the "remaining" argument, so it will be called + // when we're at that position + + // Find the CrudCommand for this type + try { + ServiceReference[] refs = bundleContext.getAllServiceReferences(CrudCommand.class.getName(), null); + if (refs != null) { + for (ServiceReference ref : refs) { + CrudCommand cmd = (CrudCommand) bundleContext.getService(ref); + try { + if (cmd.getObjectType().equals(type)) { + // Get the prefix from what the user has typed so far + // StringsCompleter will handle the final matching, but we need prefix for server-side filtering + String prefix = extractPrefixFromBuffer(commandLine); + + List completions = cmd.completeId(prefix); + + StringsCompleter delegate = new StringsCompleter(); + delegate.getStrings().addAll(completions); + return delegate.complete(session, commandLine, candidates); + } + } finally { + bundleContext.ungetService(ref); + } + } + } + } catch (Exception e) { + // Log error but continue + // Note: Printing during completion can interfere with completion, but using console for consistency + // Only log if it's a serious error - avoid logging during normal completion + LOGGER.debug("Error getting completions", e); + } + + return -1; + } + + /** + * Extract the prefix from the command line buffer for completion. + * This extracts the last word being typed, skipping options that start with '-'. + * + * @param commandLine the command line + * @return the prefix to use for filtering completions, or empty string if none + */ + private String extractPrefixFromBuffer(CommandLine commandLine) { + String buffer = commandLine.getBuffer(); + if (buffer == null || buffer.trim().isEmpty()) { + return ""; + } + + // Get the last word from the buffer (the current value being typed) + String trimmed = buffer.trim(); + int lastSpace = trimmed.lastIndexOf(' '); + if (lastSpace < 0 || lastSpace >= trimmed.length() - 1) { + return ""; + } + + String prefix = trimmed.substring(lastSpace + 1); + // Skip if it looks like an option + if (prefix.startsWith("-")) { + return ""; + } + + return prefix; + } +} diff --git a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/completers/OperationCompleter.java b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/completers/OperationCompleter.java new file mode 100644 index 0000000000..7ae6e3c35c --- /dev/null +++ b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/completers/OperationCompleter.java @@ -0,0 +1,37 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.unomi.shell.dev.completers; + +import org.apache.karaf.shell.api.action.lifecycle.Service; +import org.apache.karaf.shell.api.console.CommandLine; +import org.apache.karaf.shell.api.console.Completer; +import org.apache.karaf.shell.api.console.Session; +import org.apache.karaf.shell.support.completers.StringsCompleter; + +import java.util.List; + +@Service +public class OperationCompleter implements Completer { + private static final List OPERATIONS = List.of("create", "read", "update", "delete", "list", "help"); + + @Override + public int complete(Session session, CommandLine commandLine, List candidates) { + StringsCompleter delegate = new StringsCompleter(); + delegate.getStrings().addAll(OPERATIONS); + return delegate.complete(session, commandLine, candidates); + } +} diff --git a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/completers/TypeCompleter.java b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/completers/TypeCompleter.java new file mode 100644 index 0000000000..4a962c9c9b --- /dev/null +++ b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/completers/TypeCompleter.java @@ -0,0 +1,66 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.unomi.shell.dev.completers; + +import org.apache.karaf.shell.api.action.lifecycle.Reference; +import org.apache.karaf.shell.api.action.lifecycle.Service; +import org.apache.karaf.shell.api.console.CommandLine; +import org.apache.karaf.shell.api.console.Completer; +import org.apache.karaf.shell.api.console.Session; +import org.apache.karaf.shell.support.completers.StringsCompleter; +import org.apache.unomi.shell.dev.services.CrudCommand; +import org.osgi.framework.BundleContext; +import org.osgi.framework.ServiceReference; + +import java.io.PrintStream; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +@Service +public class TypeCompleter implements Completer { + + @Reference + private BundleContext bundleContext; + + @Override + public int complete(Session session, CommandLine commandLine, List candidates) { + Set types = new HashSet<>(); + try { + ServiceReference[] refs = bundleContext.getAllServiceReferences(CrudCommand.class.getName(), null); + if (refs != null) { + for (ServiceReference ref : refs) { + CrudCommand cmd = (CrudCommand) bundleContext.getService(ref); + try { + types.add(cmd.getObjectType()); + } finally { + bundleContext.ungetService(ref); + } + } + } + } catch (Exception e) { + // Log error but continue + // Note: Printing during completion can interfere with completion, but using console for consistency + PrintStream console = session.getConsole(); + console.println("Error: Error getting object types: " + e.getMessage()); + } + + StringsCompleter delegate = new StringsCompleter(); + delegate.getStrings().addAll(types); + return delegate.complete(session, commandLine, candidates); + } +} diff --git a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/services/BaseCrudCommand.java b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/services/BaseCrudCommand.java new file mode 100644 index 0000000000..f9b0b204d7 --- /dev/null +++ b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/services/BaseCrudCommand.java @@ -0,0 +1,400 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.unomi.shell.dev.services; + +import org.apache.commons.csv.CSVFormat; +import org.apache.commons.csv.CSVPrinter; +import org.apache.karaf.shell.api.action.Argument; +import org.apache.karaf.shell.api.action.Option; +import org.apache.karaf.shell.support.table.ShellTable; + +import java.io.PrintStream; +import org.apache.unomi.api.Item; +import org.apache.unomi.api.Metadata; +import org.apache.unomi.api.PartialList; +import org.apache.unomi.api.conditions.Condition; +import org.apache.unomi.api.conditions.ConditionType; +import org.apache.unomi.api.query.Query; +import org.apache.unomi.api.services.DefinitionsService; +import org.apache.unomi.common.DataTable; +import org.apache.unomi.shell.dev.commands.ListCommandSupport; +import org.osgi.service.component.annotations.Reference; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +/** + * Base class for CRUD command implementations that provides common functionality + * for listing objects in a tabular format. + */ +public abstract class BaseCrudCommand extends ListCommandSupport implements CrudCommand { + + private static final Logger LOGGER = LoggerFactory.getLogger(BaseCrudCommand.class.getName()); + + @Reference + protected volatile DefinitionsService definitionsService; + + @Argument(index = 0, name = "maxEntries", description = "The maximum number of entries to retrieve (defaults to 100)", required = false, multiValued = false) + protected int maxEntries = 100; + + @Option(name = "--csv", description = "Output in CSV format", required = false) + protected boolean csv; + + @Override + protected DataTable buildDataTable() { + PrintStream console = getConsole(); + try { + Query query = buildQuery(maxEntries); + PartialList items = getItems(query); + + printPaginationWarning(items, console); + + DataTable dataTable = new DataTable(); + for (Object item : items.getList()) { + Comparable[] rowWithTenant = buildRowWithTenant(item); + dataTable.addRow(rowWithTenant); + } + + return dataTable; + } catch (Exception e) { + LOGGER.error("Error building data table", e); + console.println("Error: " + e.getMessage()); + return new DataTable(); + } + } + + /** + * Get the sort criteria for the query. + * Default implementation sorts by last modification date. + * Override to provide different sorting. + * + * @return sort criteria (e.g., "metadata.lastModified:desc") + */ + protected String getSortBy() { + return "metadata.lastModified:desc"; + } + + /** + * Get items using the provided query. + * Implementations must override this to use their specific service. + * + * @param query the query to execute + * @return partial list of items + */ + protected abstract PartialList getItems(Query query); + + /** + * Build a row for the data table from an item. + * Implementations must override this to extract the relevant properties. + * + * @param item the item to convert to a row + * @return array of values for the row + */ + protected abstract Comparable[] buildRow(Object item); + + /** + * Get the column headers for the list output table. + * This implementation automatically prepends "Tenant" as the first column header, + * matching how tenantId is automatically prepended to rows in buildDataTable() and buildRows(). + * Subclasses should implement getHeadersWithoutTenant() to provide their specific headers. + * + * Subclasses can override this method to provide custom header handling (e.g., to skip the tenant column + * for commands like TenantCrudCommand where it would be redundant). + * + * @return array of column headers with "Tenant" as the first element (unless overridden) + */ + @Override + public String[] getHeaders() { + String[] headersWithoutTenant = getHeadersWithoutTenant(); + String[] headersWithTenant = new String[headersWithoutTenant.length + 1]; + headersWithTenant[0] = "Tenant"; + System.arraycopy(headersWithoutTenant, 0, headersWithTenant, 1, headersWithoutTenant.length); + return headersWithTenant; + } + + /** + * Get the column headers for the list output table without the "Tenant" column. + * Subclasses must implement this method to provide their specific headers. + * The "Tenant" column will be automatically prepended by getHeaders(). + * + * @return array of column headers (without "Tenant") + */ + protected abstract String[] getHeadersWithoutTenant(); + + /** + * Build a query with matchAllCondition and sort criteria. + * This is a common helper method used by buildDataTable(), buildRows(), buildCsvOutput(), and completeId(). + * + * @param limit maximum number of entries + * @return the configured query + * @throws Exception if definitions service is not available or matchAllCondition cannot be found + */ + protected Query buildQuery(int limit) throws Exception { + Query query = new Query(); + query.setLimit(limit); + + if (definitionsService == null) { + throw new Exception("Definitions service is not available"); + } + + ConditionType matchAllConditionType = definitionsService.getConditionType("matchAllCondition"); + if (matchAllConditionType == null) { + throw new Exception("No matchAllCondition available"); + } + + Condition matchAllCondition = new Condition(matchAllConditionType); + query.setCondition(matchAllCondition); + query.setSortby(getSortBy()); + + return query; + } + + /** + * Build a row array with tenant ID prepended as the first element. + * This is a common helper method used by buildDataTable(), buildRows(), and buildCsvOutput(). + * + * @param item the item to build a row from + * @return array with tenant ID as first element, followed by row data + */ + protected Comparable[] buildRowWithTenant(Object item) { + Comparable[] rowData = buildRow(item); + String tenantId = getTenantIdFromItem(item); + + // Create a new array with tenantId as the first element + Comparable[] rowWithTenant = new Comparable[rowData.length + 1]; + rowWithTenant[0] = tenantId; + System.arraycopy(rowData, 0, rowWithTenant, 1, rowData.length); + + return rowWithTenant; + } + + /** + * Print pagination warning if not all items were retrieved. + * This is a common helper method used by buildDataTable() and buildRows(). + * + * @param items the partial list of items + * @param console console for output + */ + protected void printPaginationWarning(PartialList items, PrintStream console) { + if (items.getList().size() != items.getTotalSize()) { + console.println("WARNING : Only the first " + items.getPageSize() + " items have been retrieved, there are " + items.getTotalSize() + " items registered in total. Use the maxEntries parameter to retrieve more items"); + } + } + + @Override + public void buildRows(ShellTable table, int maxEntries) { + PrintStream console = getConsole(); + try { + Query query = buildQuery(maxEntries); + PartialList items = getItems(query); + + printPaginationWarning(items, console); + + for (Object item : items.getList()) { + Comparable[] rowWithTenant = buildRowWithTenant(item); + table.addRow().addContent(rowWithTenant); + } + } catch (Exception e) { + console.println("Error: " + e.getMessage()); + LOGGER.error("Error building rows", e); + } + } + + /** + * Generate CSV output directly using commons-csv API. + * This method uses the same logic as buildRows() but outputs as CSV. + * + * @param console console for output + * @param headers column headers + * @param limit maximum number of entries + * @throws Exception if generation fails + */ + public void buildCsvOutput(PrintStream console, String[] headers, int limit) throws Exception { + Query query = buildQuery(limit); + PartialList items = getItems(query); + + // Generate CSV directly using commons-csv + CSVFormat csvFormat = CSVFormat.DEFAULT; + CSVPrinter printer = csvFormat.print(console); + + // Print header + printer.printRecord((Object[]) headers); + + // Print data rows + for (Object item : items.getList()) { + Comparable[] rowWithTenant = buildRowWithTenant(item); + + // Convert to List for CSV printer + List row = new ArrayList<>(); + for (Comparable cell : rowWithTenant) { + row.add(cell != null ? cell.toString() : ""); + } + printer.printRecord(row.toArray()); + } + + printer.close(); + } + + /** + * Extract the tenant ID from an item. + * + * @param item the item to extract tenant ID from + * @return the tenant ID or a default value if it can't be determined + */ + protected String getTenantIdFromItem(Object item) { + // Tenant column reserved for when tenant support is merged (Item#getTenantId, Tenant type, etc.). + return "n/a"; + } + + /** + * Default implementation of ID completion for all CRUD commands. + * This method fetches a limited number of items and filters their IDs based on the given prefix. + * + * @param prefix the prefix to filter IDs by + * @return a list of matching item IDs + */ + @Override + public List completeId(String prefix) { + try { + // Create a query with increased limit to provide more completions + Query query = buildQuery(50); // Higher limit for completions + + // Get items using the appropriate service method + PartialList items = getItems(query); + + // Extract IDs from the items + List ids = new ArrayList<>(); + for (Object item : items.getList()) { + String id = extractIdFromItem(item); + if (id != null && (prefix.isEmpty() || id.startsWith(prefix))) { + ids.add(id); + } + } + + return ids; + } catch (Exception e) { + LOGGER.error("Error completing IDs", e); + return List.of(); + } + } + + /** + * Get the console PrintStream, falling back to System.out if session is not available. + * This is needed because CrudCommand services retrieved via bundleContext.getService() + * may not have session injected (they're not shell command instances). + * + * @return PrintStream for console output + */ + protected PrintStream getConsole() { + if (session != null) { + return session.getConsole(); + } + return System.out; + } + + /** + * Apply pagination to a list of items based on query parameters. + * This is a helper method for implementations that need to paginate in-memory lists. + * + * @param items the full list of items + * @param query the query with offset and limit parameters + * @param the type of items in the list + * @return a PartialList with paginated results + */ + protected PartialList paginateList(List items, Query query) { + Integer offset = query.getOffset(); + Integer limit = query.getLimit(); + int start = offset == null ? 0 : offset; + int size = limit == null ? items.size() : limit; + int end = Math.min(start + size, items.size()); + + List pagedItems = items.subList(start, end); + return new PartialList<>(pagedItems, start, pagedItems.size(), items.size(), PartialList.Relation.EQUAL); + } + + /** + * Filter property names by prefix. This is a helper method for completePropertyNames implementations. + * + * @param propertyNames the list of property names to filter + * @param prefix the prefix to filter by + * @return filtered list of property names + */ + protected List filterPropertyNames(List propertyNames, String prefix) { + return propertyNames.stream() + .filter(name -> name.startsWith(prefix)) + .collect(Collectors.toList()); + } + + /** + * Extract the ID from an item. This method attempts to extract the ID using common patterns. + * Subclasses can override this method to provide specialized ID extraction for specific item types. + * + * @param item the item to extract the ID from + * @return the extracted ID, or null if it couldn't be extracted + */ + protected String extractIdFromItem(Object item) { + // Handle Item subclasses + if (item instanceof Item) { + return ((Item) item).getItemId(); + } + + // Handle Metadata objects + if (item instanceof Metadata) { + return ((Metadata) item).getId(); + } + + // Try reflection as a fallback + try { + // Try common getter method names for ID + for (String methodName : new String[]{"getId", "getItemId", "getIdentifier", "getKey", "getName"}) { + try { + Method method = item.getClass().getMethod(methodName); + Object result = method.invoke(item); + if (result != null) { + return result.toString(); + } + } catch (NoSuchMethodException e) { + // Method doesn't exist, try the next one + } + } + + // Try direct field access as a last resort + for (String fieldName : new String[]{"id", "itemId", "identifier", "key", "name"}) { + try { + Field field = item.getClass().getDeclaredField(fieldName); + field.setAccessible(true); + Object value = field.get(item); + if (value != null) { + return value.toString(); + } + } catch (NoSuchFieldException e) { + // Field doesn't exist, try the next one + } + } + } catch (Exception e) { + // Ignore reflection errors + } + + // If all else fails, use toString and hope it's meaningful + return item.toString(); + } +} diff --git a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/services/CrudCommand.java b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/services/CrudCommand.java new file mode 100644 index 0000000000..4c088e906b --- /dev/null +++ b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/services/CrudCommand.java @@ -0,0 +1,206 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.unomi.shell.dev.services; + +import org.apache.karaf.shell.support.table.ShellTable; + +import java.io.PrintStream; +import java.util.List; +import java.util.Map; + +/** + * Interface for implementing CRUD (Create, Read, Update, Delete) operations in Karaf shell commands for Unomi objects. + * This interface is designed to be implemented by OSGi components that provide shell command functionality for different + * types of Unomi objects (e.g., rules, segments, profiles, events). + * + * Implementations must: + * 1. Be annotated with @Component(service = CrudCommand.class, immediate = true) + * 2. Reference the appropriate Unomi service using @Reference + * 3. Handle JSON serialization/deserialization of objects + * 4. Provide property name and value completion for shell command auto-completion + * 5. Use server-side pagination and sorting in list operations to handle large datasets + * 6. Extend ListCommandSupport to implement rich tabular output + * + * The list functionality is provided by extending ListCommandSupport and implementing: + * 1. getHeaders() - Define column headers for the table + * 2. buildDataTable() - Build table data using Query with proper sorting and pagination + * 3. getSortBy() - Define sort criteria (default: metadata.lastModified:desc) + * 4. buildRow() - Convert an item to a table row + * + * Example usage in shell: + * unomi:crud create rule --file rule.json + * unomi:crud create rule -d={"itemId":"rule_id","enabled":true} + * unomi:crud read rule -i rule_id + * unomi:crud update rule -i rule_id --file updated_rule.json + * unomi:crud update rule -i rule_id -d={"itemId":"rule_id","enabled":false} + * unomi:crud delete rule -i rule_id + * unomi:crud list rule [--csv] + * unomi:crud help rule + */ +public interface CrudCommand { + /** + * Get the type of object this command handles. This is used to register the command + * for a specific object type in the Unomi shell command system. + * + * @return the object type identifier (e.g., "rule", "segment", "profile", "event") + */ + String getObjectType(); + + /** + * Create a new object in Unomi from a map of properties. The properties are typically + * deserialized from a JSON file provided by the user. + * + * Implementations should: + * 1. Convert the properties map to the appropriate Unomi object type + * 2. Validate required properties are present + * 3. Set any default values or metadata + * 4. Save the object using the appropriate Unomi service + * + * @param properties the object properties from JSON + * @return the ID of the created object + * @throws IllegalArgumentException if required properties are missing or invalid + */ + String create(Map properties); + + /** + * Read an object by ID and return its properties. The properties will be serialized + * to JSON for display to the user. + * + * Implementations should: + * 1. Load the object using the appropriate Unomi service + * 2. Return null if the object doesn't exist + * 3. Convert the object to a map of properties + * + * @param id the object ID + * @return map of object properties, or null if not found + */ + Map read(String id); + + /** + * Update an existing object with new properties. The properties are typically + * deserialized from a JSON file provided by the user. + * + * Implementations should: + * 1. Ensure the ID in properties matches the target ID + * 2. Convert the properties map to the appropriate Unomi object type + * 3. Validate required properties are present + * 4. Update the object using the appropriate Unomi service + * + * @param id the object ID to update + * @param properties the new object properties from JSON + * @throws IllegalArgumentException if required properties are missing or invalid + */ + void update(String id, Map properties); + + /** + * Delete an object by ID. + * + * Implementations should: + * 1. Delete the object using the appropriate Unomi service + * 2. Handle any cleanup or cascading deletes if necessary + * + * @param id the object ID to delete + */ + void delete(String id); + + /** + * Get help text describing the properties supported by this object type. + * This is displayed when the user runs the help command. + * + * The help text should include: + * 1. Required properties with descriptions + * 2. Optional properties with descriptions + * 3. Property types and formats + * 4. Examples or additional notes + * + * @return formatted help text for object properties + */ + String getPropertiesHelp(); + + /** + * Get completions for object IDs based on a prefix. Used for shell command + * auto-completion when the user is entering an object ID. + * + * Implementations should use their service's query capabilities to efficiently + * search for matching IDs rather than loading all objects. + * + * @param prefix the current input prefix to filter completions + * @return list of matching object IDs + */ + default List completeId(String prefix) { + // Implementations should override this with an efficient query + return List.of(); + } + + /** + * Get completions for property names based on a prefix. Used for shell command + * auto-completion when the user is editing a JSON file. + * + * Implementations should: + * 1. Define a static list of valid property names + * 2. Filter the list by the given prefix + * + * @param prefix the current input prefix to filter completions + * @return list of matching property names + */ + default List completePropertyNames(String prefix) { + return List.of(); // Default implementation returns no completions + } + + /** + * Get completions for property values based on the property name and prefix. + * Used for shell command auto-completion when the user is editing a JSON file. + * + * Implementations should: + * 1. Handle specific property names that have predefined values + * 2. Filter the possible values by the given prefix + * 3. Return empty list for properties without predefined values + * + * @param propertyName the property being completed + * @param prefix the current input prefix to filter completions + * @return list of possible values for the property + */ + default List completePropertyValue(String propertyName, String prefix) { + return List.of(); // Default implementation returns no completions + } + + /** + * Get the column headers for the list output table. + * + * @return array of column headers + */ + String[] getHeaders(); + + /** + * Build the rows for the list output table. + * + * @param table the table to add rows to + * @param maxEntries maximum number of entries to include + */ + void buildRows(ShellTable table, int maxEntries); + + /** + * Generate CSV output directly using commons-csv API. + * This method uses the same logic as buildRows() but outputs as CSV. + * + * @param console console for output + * @param headers column headers + * @param limit maximum number of entries + * @throws Exception if generation fails + */ + void buildCsvOutput(PrintStream console, String[] headers, int limit) throws Exception; +} From 51aa61c9fd49040c5093ded5bbdc1c9bfc4a747d Mon Sep 17 00:00:00 2001 From: Serge Huber Date: Wed, 6 May 2026 08:33:53 +0200 Subject: [PATCH 03/21] UNOMI-888: Javadoc and API cleanup for import/export router extension Add and expand Javadoc across router-api and router-core: IRouterCamelContext facade, ImportExportConfiguration and services, ProfileToImport, route builders, processors, aggregation strategies, and related exceptions. Also include small behavioral and API fixes discovered during documentation: - IRouterCamelContext: document methods and add getCamelContext() returning Object to avoid a compile-time Camel dependency for API consumers - RouterCamelContext: honor setTracing(boolean) (previously always enabled tracing); add @Override and {@inheritDoc} for interface methods - ImportExportConfiguration: fix setColumnSeparator to null-check the parameter, consistent with setLineSeparator - LineSplitFailureHandler: add @Override on process(Exchange) Replaces placeholder 'Created by' class comments with module-oriented descriptions. --- .../unomi/router/api/IRouterCamelContext.java | 65 +++++++++- .../router/api/ImportExportConfiguration.java | 122 ++++++++++++------ .../unomi/router/api/ProfileToImport.java | 80 ++++++++++-- .../BadProfileDataFormatException.java | 41 +++++- .../ImportExportConfigurationService.java | 44 ++++++- .../api/services/ProfileExportService.java | 57 +++++++- .../api/services/ProfileImportService.java | 47 ++++++- .../router/core/bean/CollectProfileBean.java | 28 +++- .../core/context/RouterCamelContext.java | 48 ++++++- .../ExportRouteCompletionProcessor.java | 45 ++++++- .../ImportConfigByFileNameProcessor.java | 41 +++++- .../ImportRouteCompletionProcessor.java | 53 +++++++- .../core/processor/LineBuildProcessor.java | 29 ++++- .../processor/LineSplitFailureHandler.java | 37 +++++- .../core/processor/LineSplitProcessor.java | 54 +++++++- .../core/processor/UnomiStorageProcessor.java | 42 +++++- .../ProfileExportCollectRouteBuilder.java | 51 +++++++- .../ProfileExportProducerRouteBuilder.java | 50 ++++++- .../ProfileImportFromSourceRouteBuilder.java | 53 +++++++- .../ProfileImportOneShotRouteBuilder.java | 53 +++++++- .../ProfileImportToUnomiRouteBuilder.java | 49 ++++++- .../route/RouterAbstractRouteBuilder.java | 72 ++++++++++- .../ArrayListAggregationStrategy.java | 33 ++++- .../StringLinesAggregationStrategy.java | 32 ++++- 24 files changed, 1150 insertions(+), 76 deletions(-) diff --git a/extensions/router/router-api/src/main/java/org/apache/unomi/router/api/IRouterCamelContext.java b/extensions/router/router-api/src/main/java/org/apache/unomi/router/api/IRouterCamelContext.java index 5ec1adb57a..b525ddc474 100644 --- a/extensions/router/router-api/src/main/java/org/apache/unomi/router/api/IRouterCamelContext.java +++ b/extensions/router/router-api/src/main/java/org/apache/unomi/router/api/IRouterCamelContext.java @@ -17,15 +17,78 @@ package org.apache.unomi.router.api; /** - * Created by amidani on 18/10/2017. + * Facade for the Apache Camel runtime used by the Unomi Router extension. + * Implementations manage dynamic routes for profile import (from sources such as Kafka or files) + * and profile export (collection and producer pipelines), and expose a minimal API so callers do not + * depend on Camel types unless they choose to. + * + *

    Key responsibilities: + *

      + *
    • Removing obsolete Camel route definitions when configurations change or are deleted
    • + *
    • Rebuilding import reader routes after an {@link org.apache.unomi.router.api.ImportConfiguration} update
    • + *
    • Rebuilding export reader routes after an {@link org.apache.unomi.router.api.ExportConfiguration} update
    • + *
    • Optional Camel tracing for troubleshooting route execution
    • + *
    + *

    + * + *

    Typical usage: + *

      + *
    • Management services call update methods when import/export configuration documents change
    • + *
    • Cleanup paths call {@link #killExistingRoute(String, boolean)} to drop routes whose configs were removed
    • + *
    + *

    + * + * @see org.apache.unomi.router.core.context.RouterCamelContext + * @since 1.0 */ public interface IRouterCamelContext { + /** + * Stops and removes an existing Camel route by id, if it is currently registered in the context. + * + * @param routeId Camel route identifier (usually aligned with import/export configuration id) + * @param fireEvent when {@code true}, signals that router lifecycle events may be emitted; the concrete + * implementation defines whether events are fired (reserved hook for observability) + * @throws Exception if Camel fails to remove the route definition + */ void killExistingRoute(String routeId, boolean fireEvent) throws Exception; + /** + * Refreshes the profile import reader route for the given configuration: removes any existing route with the + * same id, loads the {@link org.apache.unomi.router.api.ImportConfiguration}, and—for recurrent configs— + * registers a new route built from current settings. + * + * @param configId identifier of the import configuration whose reader route should be updated + * @param fireEvent when {@code true}, signals that router lifecycle events may be emitted after the update + * @throws Exception if route removal or registration fails + */ void updateProfileImportReaderRoute(String configId, boolean fireEvent) throws Exception; + /** + * Refreshes the profile export reader (collect) route for the given configuration: removes any existing route + * with the same id, loads the {@link org.apache.unomi.router.api.ExportConfiguration}, and—for recurrent + * configs—registers a new collect route built from current settings. + * + * @param configId identifier of the export configuration whose reader route should be updated + * @param fireEvent when {@code true}, signals that router lifecycle events may be emitted after the update + * @throws Exception if route removal or registration fails + */ void updateProfileExportReaderRoute(String configId, boolean fireEvent) throws Exception; + /** + * Enables or disables Camel route tracing on the underlying {@code CamelContext} for debugging (message flow, + * exchanges). Intended for diagnostics in development or incident analysis; may have performance impact when on. + * + * @param tracing {@code true} to enable Camel tracing, {@code false} to disable + */ void setTracing(boolean tracing); + + /** + * Returns the underlying Camel context instance. + * The API uses {@link Object} so consumers of this module are not required to depend on Camel at compile time. + * Callers that ship Camel may cast to {@code org.apache.camel.CamelContext}. + * + * @return the Camel context instance, or {@code null} if not initialized + */ + Object getCamelContext(); } diff --git a/extensions/router/router-api/src/main/java/org/apache/unomi/router/api/ImportExportConfiguration.java b/extensions/router/router-api/src/main/java/org/apache/unomi/router/api/ImportExportConfiguration.java index 10209bd154..1695b922d0 100644 --- a/extensions/router/router-api/src/main/java/org/apache/unomi/router/api/ImportExportConfiguration.java +++ b/extensions/router/router-api/src/main/java/org/apache/unomi/router/api/ImportExportConfiguration.java @@ -16,15 +16,50 @@ */ package org.apache.unomi.router.api; - import org.apache.unomi.api.Item; +import org.apache.unomi.api.Item; - import java.util.ArrayList; - import java.util.HashMap; - import java.util.List; - import java.util.Map; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; /** - * Created by amidani on 21/06/2017. + * Base configuration class for import and export operations in Apache Unomi. + * This class serves as the foundation for both ImportConfiguration and ExportConfiguration, + * providing common configuration properties and behaviors needed for data transfer operations. + * + *

    Key features and responsibilities: + *

      + *
    • Defines common configuration properties for import/export operations
    • + *
    • Manages separators and delimiters for CSV-like file formats
    • + *
    • Tracks execution status and history
    • + *
    • Handles configuration activation/deactivation
    • + *
    + *

    + * + *

    Usage in Unomi: + *

      + *
    • Used by ImportExportConfigurationService to manage data transfer configurations
    • + *
    • Consumed by Camel routes to determine how to process data
    • + *
    • Referenced by import/export processors to format data correctly
    • + *
    + *

    + * + *

    Configuration properties include: + *

      + *
    • name - unique identifier for the configuration
    • + *
    • configType - type of configuration (import/export)
    • + *
    • columnSeparator - character used to separate columns (default: ",")
    • + *
    • lineSeparator - character used to separate lines (default: "\n")
    • + *
    • multiValueSeparator - character used to separate multiple values (default: ";")
    • + *
    • active - whether the configuration is currently active
    • + *
    • status - current status of the configuration
    • + *
    • executions - history of execution attempts
    • + *
    + *

    + * + * @see org.apache.unomi.router.api.services.ImportExportConfigurationService + * @since 1.0 */ public class ImportExportConfiguration extends Item { @@ -53,28 +88,32 @@ public void setProperty(String name, Object value) { } /** - * Retrieves the name of the import configuration - * @return the name of the import configuration + * Retrieves the display name of this configuration. + * + * @return the name of this configuration */ public String getName() { return this.name; } /** - * Sets the name of the import configuration - * @param name the name of the import configuration + * Sets the display name of this configuration. + * + * @param name the name of this configuration */ public void setName(String name) { this.name = name; } /** - * Retrieves the description of the import configuration - * @return the description of the import configuration + * Retrieves the human-readable description of this configuration. + * + * @return the description of this configuration */ public String getDescription() { return this.description; } /** - * Sets the description of the import configuration - * @param description the description of the import configuration + * Sets the human-readable description of this configuration. + * + * @param description the description of this configuration */ public void setDescription(String description) { this.description = description; @@ -82,14 +121,16 @@ public void setDescription(String description) { /** - * Retrieves the config type of the import configuration - * @return the config type of the import configuration + * Retrieves the configuration type (for example import vs export semantics used by the router). + * + * @return the config type of this configuration */ public String getConfigType() { return this.configType; } /** - * Sets the config type of the import configuration - * @param configType the config type of the import configuration + * Sets the configuration type. + * + * @param configType the config type for this configuration */ public void setConfigType(String configType) { this.configType = configType; @@ -106,45 +147,45 @@ public Object getProperty(String name) { } /** - * Retrieves a Map of all property name - value pairs for this import configuration. + * Retrieves a map of all property name/value pairs for this configuration. * - * @return a Map of all property name - value pairs for this import configuration + * @return a map of all property name/value pairs for this configuration */ public Map getProperties() { return properties; } /** - * Retrieves the import configuration active flag. + * Returns whether this configuration is active (eligible for scheduled or triggered runs). * - * @return true if the import configuration is active false if not + * @return {@code true} if this configuration is active, {@code false} otherwise */ public boolean isActive() { return this.active; } /** - * Sets the active flag true/false. + * Sets whether this configuration is active. * - * @param active a boolean to set to active or inactive the import configuration + * @param active {@code true} to activate, {@code false} to deactivate */ public void setActive(boolean active) { this.active = active; } /** - * Retrieves the import configuration status for last execution. + * Retrieves the status of the last execution for this configuration. * - * @return status of the last execution + * @return status of the last execution, or {@code null} if none */ public String getStatus() { return this.status; } /** - * Sets status of the last execution. + * Sets the status of the last execution for this configuration. * - * @param status of the last execution + * @param status the status of the last execution */ public void setStatus(String status) { this.status = status; @@ -159,11 +200,12 @@ public String getColumnSeparator() { } /** - * Sets the column separator. - * @param columnSeparator property used to specify a line separator. Defaults to ',' + * Sets the column separator used when reading or writing delimited text (typically CSV). + * + * @param columnSeparator the column delimiter; defaults to {@code ","} when not overridden */ public void setColumnSeparator(String columnSeparator) { - if(this.columnSeparator !=null) { + if (columnSeparator != null) { this.columnSeparator = columnSeparator; } } @@ -187,9 +229,9 @@ public void setLineSeparator(String lineSeparator) { } /** - * Gets the multi value separator. + * Returns the separator used between multiple values within a single field. * - * @return multiValueSeparator multi value separator + * @return the multi-value separator (often {@code ";"}) */ public String getMultiValueSeparator() { return this.multiValueSeparator; @@ -206,9 +248,9 @@ public void setMultiValueSeparator(String multiValueSeparator) { } /** - * Gets the multi value delimiter. + * Returns the delimiter wrapping multi-valued fields when serialized. * - * @return multiValueDelimiter multi value delimiter + * @return the multi-value delimiter (may be empty when not used) */ public String getMultiValueDelimiter() { return this.multiValueDelimiter; @@ -225,8 +267,9 @@ public void setMultiValueDelimiter(String multiValueDelimiter) { } /** - * Retrieves the executions - * @return executions + * Returns the history of execution records for this configuration (timestamps, counts, errors, etc.). + * + * @return the list of execution maps; may be empty */ public List> getExecutions() { return this.executions; @@ -234,8 +277,9 @@ public List> getExecutions() { /** - * Sets the executions - * @param executions executions + * Replaces the execution history for this configuration. + * + * @param executions the new execution history list */ public void setExecutions(List> executions) { this.executions = executions; diff --git a/extensions/router/router-api/src/main/java/org/apache/unomi/router/api/ProfileToImport.java b/extensions/router/router-api/src/main/java/org/apache/unomi/router/api/ProfileToImport.java index 30e40e0c81..bb87cd375f 100644 --- a/extensions/router/router-api/src/main/java/org/apache/unomi/router/api/ProfileToImport.java +++ b/extensions/router/router-api/src/main/java/org/apache/unomi/router/api/ProfileToImport.java @@ -22,56 +22,118 @@ import java.util.List; /** - * An extension of {@link Profile} to handle merge strategy and deletion when importing profiles + * A specialized Profile class designed for import operations in Apache Unomi. + * This class extends the standard {@link Profile} with additional properties and behaviors + * specifically needed during the profile import process. + * + *

    Key features: + *

      + *
    • Controls which properties should be overwritten during import
    • + *
    • Specifies the property used for merging with existing profiles
    • + *
    • Handles profile deletion flags
    • + *
    • Controls overwrite behavior for existing profiles
    • + *
    + *

    + * + *

    Usage in Unomi: + *

      + *
    • Used by import processors to handle profile data
    • + *
    • Consumed by ProfileImportService for import operations
    • + *
    • Supports different import strategies (merge/overwrite/delete)
    • + *
    + *

    + * + * @see Profile + * @see org.apache.unomi.router.api.services.ProfileImportService + * @since 1.0 */ public class ProfileToImport extends Profile { + /** List of property names that should be overwritten during import */ private List propertiesToOverwrite; + + /** Property used to identify existing profiles for merging */ private String mergingProperty; + + /** Flag indicating if this profile should be deleted */ private boolean profileToDelete; - private boolean overwriteExistingProfiles; + /** Flag controlling whether to overwrite existing profile data */ + private boolean overwriteExistingProfiles; + /** + * Gets the list of properties that should be overwritten during import. + * These properties will be updated even if they already exist in the target profile. + * + * @return list of property names to overwrite + */ public List getPropertiesToOverwrite() { return this.propertiesToOverwrite; } + /** + * Sets the list of properties that should be overwritten during import. + * + * @param propertiesToOverwrite list of property names that should be overwritten + */ public void setPropertiesToOverwrite(List propertiesToOverwrite) { this.propertiesToOverwrite = propertiesToOverwrite; } + /** + * Checks if this profile is marked for deletion. + * When true, the matching profile in the system will be deleted rather than updated. + * + * @return true if the profile should be deleted, false otherwise + */ public boolean isProfileToDelete() { return this.profileToDelete; } + /** + * Sets whether this profile should be deleted during import. + * + * @param profileToDelete true to mark the profile for deletion, false otherwise + */ public void setProfileToDelete(boolean profileToDelete) { this.profileToDelete = profileToDelete; } + /** + * Checks if existing profiles should be overwritten during import. + * When true, all properties of existing profiles will be overwritten with imported data. + * + * @return true if existing profiles should be overwritten, false for selective updates + */ public boolean isOverwriteExistingProfiles() { return this.overwriteExistingProfiles; } /** - * Sets the overwriteExistingProfiles flag. - * @param overwriteExistingProfiles flag used to specify if we want to overwrite existing profiles + * Sets whether existing profiles should be completely overwritten during import. + * + * @param overwriteExistingProfiles true to overwrite all properties, false for selective updates */ public void setOverwriteExistingProfiles(boolean overwriteExistingProfiles) { this.overwriteExistingProfiles = overwriteExistingProfiles; } + /** + * Gets the property name used for identifying existing profiles during merge operations. + * This property is used to match imported profiles with existing ones in the system. + * + * @return the name of the property used for profile matching + */ public String getMergingProperty() { return this.mergingProperty; } /** - * Sets the merging property. - * @param mergingProperty property used to check if the profile exist when merging + * Sets the property name used for identifying existing profiles during merge operations. + * + * @param mergingProperty the name of the property to use for profile matching */ public void setMergingProperty(String mergingProperty) { this.mergingProperty = mergingProperty; } - - - } diff --git a/extensions/router/router-api/src/main/java/org/apache/unomi/router/api/exceptions/BadProfileDataFormatException.java b/extensions/router/router-api/src/main/java/org/apache/unomi/router/api/exceptions/BadProfileDataFormatException.java index 85cf5ea807..731e377e5c 100644 --- a/extensions/router/router-api/src/main/java/org/apache/unomi/router/api/exceptions/BadProfileDataFormatException.java +++ b/extensions/router/router-api/src/main/java/org/apache/unomi/router/api/exceptions/BadProfileDataFormatException.java @@ -17,18 +17,57 @@ package org.apache.unomi.router.api.exceptions; /** - * Created by amidani on 13/06/2017. + * Exception thrown when profile data cannot be properly parsed or formatted during import/export operations. + * This exception indicates issues with the structure or content of profile data that prevent it from being + * properly processed by the Unomi router. + * + *

    Common scenarios where this exception is thrown: + *

      + *
    • Invalid CSV format in import files
    • + *
    • Missing required profile fields
    • + *
    • Incorrect data types for profile properties
    • + *
    • Malformed multi-value fields
    • + *
    • Invalid date formats
    • + *
    + *

    + * + *

    Usage in Unomi: + *

      + *
    • Thrown by profile import processors
    • + *
    • Used in data validation steps
    • + *
    • Caught by error handling routes
    • + *
    + *

    + * + * @see org.apache.unomi.router.api.ProfileToImport + * @since 1.0 */ public class BadProfileDataFormatException extends Exception { + /** + * Constructs a new exception with {@code null} as its detail message. + * The cause is not initialized. + */ public BadProfileDataFormatException() { super(); } + /** + * Constructs a new exception with the specified detail message. + * The cause is not initialized. + * + * @param message the detail message describing the cause of the exception + */ public BadProfileDataFormatException(String message) { super(message); } + /** + * Constructs a new exception with the specified detail message and cause. + * + * @param message the detail message describing the cause of the exception + * @param cause the underlying cause of the exception + */ public BadProfileDataFormatException(String message, Throwable cause) { super(message, cause); } diff --git a/extensions/router/router-api/src/main/java/org/apache/unomi/router/api/services/ImportExportConfigurationService.java b/extensions/router/router-api/src/main/java/org/apache/unomi/router/api/services/ImportExportConfigurationService.java index edb103cc52..023741708f 100644 --- a/extensions/router/router-api/src/main/java/org/apache/unomi/router/api/services/ImportExportConfigurationService.java +++ b/extensions/router/router-api/src/main/java/org/apache/unomi/router/api/services/ImportExportConfigurationService.java @@ -24,7 +24,40 @@ import java.util.Map; /** - * A service to access and operate on {@link ImportConfiguration}s / {@link ExportConfiguration}s. + * Service interface for managing import and export configurations in Apache Unomi. + * This service provides CRUD operations for {@link ImportConfiguration}s and {@link ExportConfiguration}s, + * as well as functionality to manage the lifecycle of data transfer configurations. + * + *

    Key responsibilities: + *

      + *
    • Managing the lifecycle of import/export configurations
    • + *
    • Providing CRUD operations for configurations
    • + *
    • Coordinating with Camel routes for configuration updates
    • + *
    • Tracking configuration changes that need route updates
    • + *
    + *

    + * + *

    Usage in Unomi: + *

      + *
    • Used by REST endpoints to manage import/export configurations
    • + *
    • Consumed by Camel routes to get configuration updates
    • + *
    • Utilized by admin interfaces for configuration management
    • + *
    + *

    + * + *

    Implementation considerations: + *

      + *
    • Implementations should handle configuration persistence
    • + *
    • Thread safety should be considered for concurrent operations
    • + *
    • Configuration changes should be properly propagated to running routes
    • + *
    + *

    + * + * @param The type of configuration (ImportConfiguration or ExportConfiguration) + * @see ImportConfiguration + * @see ExportConfiguration + * @see RouterConstants.CONFIG_CAMEL_REFRESH + * @since 1.0 */ public interface ImportExportConfigurationService { @@ -38,7 +71,7 @@ public interface ImportExportConfigurationService { /** * Retrieves the import/export configuration identified by the specified identifier. * - * @param configId the identifier of the profile to retrieve + * @param configId the identifier of the configuration to retrieve * @return the import/export configuration identified by the specified identifier or * {@code null} if no such import/export configuration exists */ @@ -61,8 +94,11 @@ public interface ImportExportConfigurationService { void delete(String configId); /** - * Used by camel route system to get the latest changes on configs and reflect changes on camel routes if necessary - * @return map of configId per operation to be done in camel + * Consumes pending configuration changes for the Camel router layer. + * Implementations typically dequeue IDs whose configurations were updated or removed so that + * routes can be refreshed accordingly. + * + * @return a map from configuration ID to the refresh operation ({@link RouterConstants.CONFIG_CAMEL_REFRESH}) */ Map consumeConfigsToBeRefresh(); } diff --git a/extensions/router/router-api/src/main/java/org/apache/unomi/router/api/services/ProfileExportService.java b/extensions/router/router-api/src/main/java/org/apache/unomi/router/api/services/ProfileExportService.java index dc0d81df2c..37b921c788 100644 --- a/extensions/router/router-api/src/main/java/org/apache/unomi/router/api/services/ProfileExportService.java +++ b/extensions/router/router-api/src/main/java/org/apache/unomi/router/api/services/ProfileExportService.java @@ -23,12 +23,67 @@ import java.util.Collection; /** - * Created by amidani on 30/06/2017. + * Service interface for handling the export of profiles from Apache Unomi. + * This service is responsible for extracting profiles based on segment criteria + * and converting them into the appropriate export format (e.g., CSV). + * + *

    Key responsibilities: + *

      + *
    • Extracting profiles based on segment criteria
    • + *
    • Converting profiles to export format
    • + *
    • Handling data formatting and transformation
    • + *
    • Managing export file generation
    • + *
    + *

    + * + *

    Usage in Unomi: + *

      + *
    • Called by export route processors to handle profile extraction
    • + *
    • Used during scheduled export operations
    • + *
    • Integrated with Unomi's segmentation system
    • + *
    + *

    + * + *

    Implementation considerations: + *

      + *
    • Must handle large data sets efficiently
    • + *
    • Should implement proper error handling
    • + *
    • Must respect profile property formatting
    • + *
    • Should handle multi-valued properties
    • + *
    + *

    + * + * @see Profile + * @see ExportConfiguration + * @see PropertyType + * @since 1.0 */ public interface ProfileExportService { + /** + * Extracts profiles belonging to a specified segment and formats them for export. + * This method handles the bulk export operation, including: + * - Querying profiles based on segment criteria + * - Formatting profiles according to export configuration + * - Generating the export content + * + * @param exportConfiguration the configuration specifying export parameters and format + * @return a String containing the formatted export data + */ String extractProfilesBySegment(ExportConfiguration exportConfiguration); + /** + * Converts a single profile to a CSV line format according to the export configuration. + * This method handles the formatting of individual profiles, including: + * - Property selection and ordering + * - Value formatting + * - Multi-value handling + * - Line separator management + * + * @param profile the profile to convert + * @param exportConfiguration the configuration specifying the export format + * @return a String containing the CSV-formatted profile data + */ String convertProfileToCSVLine(Profile profile, ExportConfiguration exportConfiguration); } diff --git a/extensions/router/router-api/src/main/java/org/apache/unomi/router/api/services/ProfileImportService.java b/extensions/router/router-api/src/main/java/org/apache/unomi/router/api/services/ProfileImportService.java index aa7d1829d7..fc1fe931c8 100644 --- a/extensions/router/router-api/src/main/java/org/apache/unomi/router/api/services/ProfileImportService.java +++ b/extensions/router/router-api/src/main/java/org/apache/unomi/router/api/services/ProfileImportService.java @@ -21,9 +21,54 @@ import java.lang.reflect.InvocationTargetException; /** - * Created by amidani on 20/05/2017. + * Service interface for handling the import of individual profiles into Apache Unomi. + * This service is responsible for the actual processing and storage of imported profile data, + * including merging with existing profiles or creating new ones as needed. + * + *

    Key responsibilities: + *

      + *
    • Processing individual profile imports
    • + *
    • Merging imported data with existing profiles
    • + *
    • Handling profile creation for new imports
    • + *
    • Managing profile deletion when specified
    • + *
    + *

    + * + *

    Usage in Unomi: + *

      + *
    • Called by import route processors to handle individual profile data
    • + *
    • Used during batch import operations
    • + *
    • Integrated with Unomi's profile management system
    • + *
    + *

    + * + *

    Implementation considerations: + *

      + *
    • Must handle profile merging strategies
    • + *
    • Should implement proper error handling
    • + *
    • Must maintain data consistency
    • + *
    • Should handle property type conversions
    • + *
    + *

    + * + * @see ProfileToImport + * @see org.apache.unomi.api.Profile + * @since 1.0 */ public interface ProfileImportService { + /** + * Processes a profile for import, handling the save, merge, or delete operation as specified. + * This method is the core functionality for profile import processing, determining whether to: + * - Create a new profile + * - Merge with an existing profile + * - Delete an existing profile + * + * @param profileToImport the profile data to be imported, containing all necessary information + * for the import operation including the operation type + * @return true if the operation was successful, false otherwise + * @throws InvocationTargetException if there is an error during property mapping + * @throws IllegalAccessException if there is an error accessing profile properties + */ boolean saveMergeDeleteImportedProfile(ProfileToImport profileToImport) throws InvocationTargetException, IllegalAccessException; } diff --git a/extensions/router/router-core/src/main/java/org/apache/unomi/router/core/bean/CollectProfileBean.java b/extensions/router/router-core/src/main/java/org/apache/unomi/router/core/bean/CollectProfileBean.java index 1ea03eb3a6..43eb8f61da 100644 --- a/extensions/router/router-core/src/main/java/org/apache/unomi/router/core/bean/CollectProfileBean.java +++ b/extensions/router/router-core/src/main/java/org/apache/unomi/router/core/bean/CollectProfileBean.java @@ -22,17 +22,43 @@ import java.util.List; /** - * Created by amidani on 28/06/2017. + * A bean that handles the collection of profiles based on segment criteria. + * This class provides functionality to extract profiles from Unomi's persistence + * layer based on segment membership. + * + *

    Features: + *

      + *
    • Segment-based profile extraction
    • + *
    • Integration with Unomi's persistence service
    • + *
    • Batch profile retrieval capabilities
    • + *
    + *

    + * + * @since 1.0 */ public class CollectProfileBean { private PersistenceService persistenceService; + /** + * Returns all profiles that belong to the given segment. + *

    + * Note: the current implementation may load a large result set into memory; see UNOMI-759. + *

    + * + * @param segment the segment identifier to match (stored index {@code "segments"}) + * @return profiles for that segment; may be empty, never {@code null} + */ public List extractProfileBySegment(String segment) { // TODO: UNOMI-759 avoid loading all profiles in RAM here return persistenceService.query("segments", segment,null, Profile.class); } + /** + * Sets the persistence service used for profile queries. + * + * @param persistenceService the Unomi persistence service to use + */ public void setPersistenceService(PersistenceService persistenceService) { this.persistenceService = persistenceService; } diff --git a/extensions/router/router-core/src/main/java/org/apache/unomi/router/core/context/RouterCamelContext.java b/extensions/router/router-core/src/main/java/org/apache/unomi/router/core/context/RouterCamelContext.java index 4d329209d3..ae23e5ebc4 100644 --- a/extensions/router/router-core/src/main/java/org/apache/unomi/router/core/context/RouterCamelContext.java +++ b/extensions/router/router-core/src/main/java/org/apache/unomi/router/core/context/RouterCamelContext.java @@ -49,7 +49,25 @@ import java.util.concurrent.TimeUnit; /** - * Created by amidani on 04/05/2017. + * The main Camel context manager for the Unomi Router component. + * This class manages the lifecycle of all import and export routes, + * handles route configuration updates, and maintains the Camel context. + * + *

    Features: + *

      + *
    • Initializes and manages the Camel context
    • + *
    • Sets up import and export routes
    • + *
    • Handles route configuration updates
    • + *
    • Manages route lifecycle (start/stop/update)
    • + *
    • Provides monitoring through event notifications
    • + *
    • Supports both Kafka and direct endpoints
    • + *
    + *

    + * + *

    Dependency-injection setters on this class are intended for OSGi/Blueprint wiring and are not part of the + * {@link IRouterCamelContext} API surface.

    + * + * @since 1.0 */ public class RouterCamelContext implements IRouterCamelContext { @@ -79,8 +97,11 @@ public class RouterCamelContext implements IRouterCamelContext { private Integer configsRefreshInterval = 1000; private ScheduledFuture scheduledFuture; + /** Event topic fired when a router configuration or route is removed (reserved for integrations). */ public static String EVENT_ID_REMOVE = "org.apache.unomi.router.event.remove"; + /** Event topic related to import lifecycle (reserved for integrations). */ public static String EVENT_ID_IMPORT = "org.apache.unomi.router.event.import"; + /** Event topic related to export lifecycle (reserved for integrations). */ public static String EVENT_ID_EXPORT = "org.apache.unomi.router.event.export"; public void setExecHistorySize(String execHistorySize) { @@ -99,10 +120,17 @@ public void setConfigSharingService(ConfigSharingService configSharingService) { this.configSharingService = configSharingService; } + /** {@inheritDoc} */ + @Override public void setTracing(boolean tracing) { - camelContext.setTracing(true); + camelContext.setTracing(tracing); } + /** + * Initializes the scheduler, shared config properties, the Camel context, and import/export routes. + * + * @throws Exception if Camel or service setup fails + */ public void init() throws Exception { LOGGER.info("Initialize Camel Context..."); scheduler = Executors.newSingleThreadScheduledExecutor(); @@ -116,6 +144,11 @@ public void init() throws Exception { LOGGER.info("Camel Context initialized successfully."); } + /** + * Stops the configuration refresh scheduler and shuts down the Camel context (all routes and components). + * + * @throws Exception if Camel shutdown fails + */ public void destroy() throws Exception { scheduledFuture.cancel(true); if (scheduler != null) { @@ -223,6 +256,8 @@ private void initCamel() throws Exception { camelContext.start(); } + /** {@inheritDoc} */ + @Override public void killExistingRoute(String routeId, boolean fireEvent) throws Exception { //Active routes Route route = camelContext.getRoute(routeId); @@ -234,6 +269,8 @@ public void killExistingRoute(String routeId, boolean fireEvent) throws Exceptio } } + /** {@inheritDoc} */ + @Override public void updateProfileImportReaderRoute(String configId, boolean fireEvent) throws Exception { killExistingRoute(configId, false); @@ -255,6 +292,8 @@ public void updateProfileImportReaderRoute(String configId, boolean fireEvent) t } } + /** {@inheritDoc} */ + @Override public void updateProfileExportReaderRoute(String configId, boolean fireEvent) throws Exception { killExistingRoute(configId, false); @@ -275,6 +314,11 @@ public void updateProfileExportReaderRoute(String configId, boolean fireEvent) t } } + /** + * {@inheritDoc} + *

    The concrete type is {@link org.apache.camel.CamelContext}; callers may narrow the reference safely.

    + */ + @Override public CamelContext getCamelContext() { return camelContext; } diff --git a/extensions/router/router-core/src/main/java/org/apache/unomi/router/core/processor/ExportRouteCompletionProcessor.java b/extensions/router/router-core/src/main/java/org/apache/unomi/router/core/processor/ExportRouteCompletionProcessor.java index d9794a8de1..7f7a7e7767 100644 --- a/extensions/router/router-core/src/main/java/org/apache/unomi/router/core/processor/ExportRouteCompletionProcessor.java +++ b/extensions/router/router-core/src/main/java/org/apache/unomi/router/core/processor/ExportRouteCompletionProcessor.java @@ -30,14 +30,47 @@ import java.util.Map; /** - * Created by amidani on 29/06/2017. + * A Camel processor that handles the completion of profile export routes. + * This processor updates the export configuration with execution statistics + * and manages the execution history of export operations. + * + *

    The processor performs the following operations: + *

      + *
    • Records export execution statistics
    • + *
    • Updates the export configuration status
    • + *
    • Maintains execution history within configured size limits
    • + *
    • Persists updated configuration information
    • + *
    + *

    + * + * @since 1.0 */ public class ExportRouteCompletionProcessor implements Processor { private static final Logger LOGGER = LoggerFactory.getLogger(ExportRouteCompletionProcessor.class.getName()); + + /** Service for managing export configurations */ private ImportExportConfigurationService exportConfigurationService; + + /** Maximum number of execution history entries to maintain */ private int executionsHistorySize; + /** + * Processes the completion of an export route by updating its configuration and statistics. + * + *

    This method: + *

      + *
    • Loads the current export configuration
    • + *
    • Creates an execution entry with timestamp and statistics
    • + *
    • Updates the configuration with execution results
    • + *
    • Maintains the execution history size limit
    • + *
    • Updates the export status to complete
    • + *
    + *

    + * + * @param exchange the Camel exchange containing export execution details + * @throws Exception if an error occurs during processing + */ @Override public void process(Exchange exchange) throws Exception { // We load the conf from ES because we are going to increment the execution number @@ -59,10 +92,20 @@ public void process(Exchange exchange) throws Exception { LOGGER.info("Processing route {} completed.", exchange.getFromRouteId()); } + /** + * Sets the service used for managing export configurations. + * + * @param exportConfigurationService the service for handling export configurations + */ public void setExportConfigurationService(ImportExportConfigurationService exportConfigurationService) { this.exportConfigurationService = exportConfigurationService; } + /** + * Sets the maximum size of the execution history to maintain. + * + * @param executionsHistorySize the maximum number of execution entries to keep + */ public void setExecutionsHistorySize(int executionsHistorySize) { this.executionsHistorySize = executionsHistorySize; } diff --git a/extensions/router/router-core/src/main/java/org/apache/unomi/router/core/processor/ImportConfigByFileNameProcessor.java b/extensions/router/router-core/src/main/java/org/apache/unomi/router/core/processor/ImportConfigByFileNameProcessor.java index 61c6ed4089..39e5a42d98 100644 --- a/extensions/router/router-core/src/main/java/org/apache/unomi/router/core/processor/ImportConfigByFileNameProcessor.java +++ b/extensions/router/router-core/src/main/java/org/apache/unomi/router/core/processor/ImportConfigByFileNameProcessor.java @@ -26,14 +26,48 @@ import org.slf4j.LoggerFactory; /** - * Created by amidani on 22/05/2017. + * A Camel processor that retrieves import configurations based on file names. + * This processor extracts the configuration ID from the filename and loads + * the corresponding import configuration for processing. + * + *

    The processor expects filenames in the format: + *

    configurationId.extension
    + * where the configurationId matches an existing import configuration.

    + * + *

    Features: + *

      + *
    • Extracts configuration ID from filename
    • + *
    • Loads corresponding import configuration
    • + *
    • Sets configuration in exchange header for processing
    • + *
    • Handles missing configurations gracefully
    • + *
    + *

    + * + * @since 1.0 */ public class ImportConfigByFileNameProcessor implements Processor { private static final Logger LOGGER = LoggerFactory.getLogger(ImportConfigByFileNameProcessor.class.getName()); + /** Service for managing import configurations */ private ImportExportConfigurationService importConfigurationService; + /** + * Processes the exchange by loading an import configuration based on the filename. + * + *

    This method: + *

      + *
    • Extracts the filename from the exchange body
    • + *
    • Parses the configuration ID from the filename
    • + *
    • Attempts to load the corresponding import configuration
    • + *
    • Sets the configuration in the exchange header if found
    • + *
    • Stops route processing if no configuration is found
    • + *
    + *

    + * + * @param exchange the Camel exchange containing the file to process + * @throws Exception if an error occurs during processing + */ @Override public void process(Exchange exchange) throws Exception { @@ -49,6 +83,11 @@ public void process(Exchange exchange) throws Exception { } } + /** + * Sets the service used for managing import configurations. + * + * @param importConfigurationService the service for handling import configurations + */ public void setImportConfigurationService(ImportExportConfigurationService importConfigurationService) { this.importConfigurationService = importConfigurationService; } diff --git a/extensions/router/router-core/src/main/java/org/apache/unomi/router/core/processor/ImportRouteCompletionProcessor.java b/extensions/router/router-core/src/main/java/org/apache/unomi/router/core/processor/ImportRouteCompletionProcessor.java index 7554ab3fe2..76e82e0d8a 100644 --- a/extensions/router/router-core/src/main/java/org/apache/unomi/router/core/processor/ImportRouteCompletionProcessor.java +++ b/extensions/router/router-core/src/main/java/org/apache/unomi/router/core/processor/ImportRouteCompletionProcessor.java @@ -26,15 +26,51 @@ import java.util.*; /** - * Created by amidani on 14/06/2017. + * A Camel processor that handles the completion of profile import routes. + * This processor manages the final stage of import operations, collecting statistics, + * handling errors, and updating the import configuration with execution results. + * + *

    The processor performs the following operations: + *

      + *
    • Collects import statistics (success, failure, ignore counts)
    • + *
    • Manages error reporting with configurable limits
    • + *
    • Updates import configuration status
    • + *
    • Maintains execution history
    • + *
    • Handles both one-shot and recurring imports
    • + *
    + *

    + * + * @since 1.0 */ public class ImportRouteCompletionProcessor implements Processor { private static final Logger LOGGER = LoggerFactory.getLogger(ImportRouteCompletionProcessor.class.getName()); + + /** Service for managing import configurations */ private ImportExportConfigurationService importConfigurationService; + + /** Maximum number of execution history entries to maintain */ private int executionsHistorySize; + + /** Maximum number of errors to report per execution */ private int execErrReportSize; + /** + * Processes the completion of an import route by collecting statistics and updating configuration. + * + *

    This method: + *

      + *
    • Identifies the import configuration (one-shot or recurring)
    • + *
    • Counts successful, failed, and ignored imports
    • + *
    • Collects error information up to the configured limit
    • + *
    • Updates the import configuration with execution results
    • + *
    • Sets the final status based on success/failure counts
    • + *
    + *

    + * + * @param exchange the Camel exchange containing import results + * @throws Exception if an error occurs during processing + */ @Override public void process(Exchange exchange) throws Exception { String importConfigId = null; @@ -89,14 +125,29 @@ public void process(Exchange exchange) throws Exception { LOGGER.info("Processing route {} completed. completion date: {}.", exchange.getFromRouteId(), new Date()); } + /** + * Sets the service used for managing import configurations. + * + * @param importConfigurationService the service for handling import configurations + */ public void setImportConfigurationService(ImportExportConfigurationService importConfigurationService) { this.importConfigurationService = importConfigurationService; } + /** + * Sets the maximum size of the execution history to maintain. + * + * @param executionsHistorySize the maximum number of execution entries to keep + */ public void setExecutionsHistorySize(int executionsHistorySize) { this.executionsHistorySize = executionsHistorySize; } + /** + * Sets the maximum number of errors to report per execution. + * + * @param execErrReportSize the maximum number of errors to store per execution + */ public void setExecErrReportSize(int execErrReportSize) { this.execErrReportSize = execErrReportSize; } diff --git a/extensions/router/router-core/src/main/java/org/apache/unomi/router/core/processor/LineBuildProcessor.java b/extensions/router/router-core/src/main/java/org/apache/unomi/router/core/processor/LineBuildProcessor.java index ecfab189f8..f1748cc0af 100644 --- a/extensions/router/router-core/src/main/java/org/apache/unomi/router/core/processor/LineBuildProcessor.java +++ b/extensions/router/router-core/src/main/java/org/apache/unomi/router/core/processor/LineBuildProcessor.java @@ -23,16 +23,43 @@ import org.apache.unomi.router.api.services.ProfileExportService; /** - * Created by amidani on 28/06/2017. + * A Camel processor that converts Unomi Profile objects into CSV lines for export. + * This processor is responsible for transforming profile data into a formatted string + * according to the export configuration specified in the exchange header. + * + *

    The processor works in conjunction with the ProfileExportService to perform + * the actual conversion of profile data to CSV format.

    + * + * @since 1.0 */ public class LineBuildProcessor implements Processor { private ProfileExportService profileExportService; + /** + * Constructs a new LineBuildProcessor with the specified ProfileExportService. + * + * @param profileExportService the service responsible for converting profiles to CSV format + */ public LineBuildProcessor(ProfileExportService profileExportService) { this.profileExportService = profileExportService; } + /** + * Processes the exchange by converting a Profile object into a CSV line. + * + *

    This method: + *

      + *
    • Extracts the export configuration from the exchange header
    • + *
    • Gets the Profile object from the exchange body
    • + *
    • Converts the profile to a CSV line using the ProfileExportService
    • + *
    • Sets the resulting string as the new exchange body
    • + *
    + *

    + * + * @param exchange the Camel exchange containing the Profile to convert and export configuration + * @throws Exception if an error occurs during processing + */ @Override public void process(Exchange exchange) throws Exception { ExportConfiguration exportConfiguration = (ExportConfiguration) exchange.getIn().getHeader("exportConfig"); diff --git a/extensions/router/router-core/src/main/java/org/apache/unomi/router/core/processor/LineSplitFailureHandler.java b/extensions/router/router-core/src/main/java/org/apache/unomi/router/core/processor/LineSplitFailureHandler.java index 3cf8ff6b62..2f4a5950de 100644 --- a/extensions/router/router-core/src/main/java/org/apache/unomi/router/core/processor/LineSplitFailureHandler.java +++ b/extensions/router/router-core/src/main/java/org/apache/unomi/router/core/processor/LineSplitFailureHandler.java @@ -25,12 +25,47 @@ import org.slf4j.LoggerFactory; /** - * Created by amidani on 14/06/2017. + * A Camel processor that handles failures during the line splitting process of data import. + * This processor is responsible for creating structured error reports when lines fail to process, + * providing detailed information about the nature of the failure and the problematic data. + * + *

    The handler processes different types of exceptions: + *

      + *
    • BadProfileDataFormatException - for data format related errors
    • + *
    • General exceptions - capturing the root cause message
    • + *
    + *

    + * + *

    For each failure, it creates an ImportLineError object containing: + *

      + *
    • The error code or message
    • + *
    • The content of the failed line
    • + *
    • The line number in the source file
    • + *
    + *

    + * + * @since 1.0 */ public class LineSplitFailureHandler implements Processor { private static final Logger LOGGER = LoggerFactory.getLogger(LineSplitFailureHandler.class.getName()); + /** + * Processes failures that occur during line splitting and creates structured error reports. + * + *

    This method: + *

      + *
    • Logs the failure details including the route ID and exception
    • + *
    • Creates an ImportLineError object with detailed error information
    • + *
    • Extracts the appropriate error message based on the exception type
    • + *
    • Sets the failure information in the exchange for further processing
    • + *
    + *

    + * + * @param exchange the Camel exchange containing the failed message and exception details + * @throws Exception if an error occurs during failure handling + */ + @Override public void process(Exchange exchange) throws Exception { LOGGER.error("Route: {}, Error: {}", exchange.getProperty(Exchange.FAILURE_ROUTE_ID), exchange.getProperty(Exchange.EXCEPTION_CAUGHT)); ImportLineError importLineError = new ImportLineError(); diff --git a/extensions/router/router-core/src/main/java/org/apache/unomi/router/core/processor/LineSplitProcessor.java b/extensions/router/router-core/src/main/java/org/apache/unomi/router/core/processor/LineSplitProcessor.java index 265fec8f58..e93d556374 100644 --- a/extensions/router/router-core/src/main/java/org/apache/unomi/router/core/processor/LineSplitProcessor.java +++ b/extensions/router/router-core/src/main/java/org/apache/unomi/router/core/processor/LineSplitProcessor.java @@ -35,25 +35,77 @@ import java.util.*; /** - * Created by amidani on 29/12/2016. + * A Camel processor that splits and processes CSV lines into ProfileToImport objects. + * This processor handles the conversion of CSV data into structured profile data according + * to the import configuration, supporting various data types and multi-value fields. + * + *

    Features include: + *

      + *
    • CSV parsing using RFC4180 standard
    • + *
    • Support for header rows
    • + *
    • Field mapping to profile properties
    • + *
    • Multi-value field handling
    • + *
    • Type conversion based on property definitions
    • + *
    • Profile merging configuration
    • + *
    • Delete operation support
    • + *
    + *

    + * + * @since 1.0 */ public class LineSplitProcessor implements Processor { private static final Logger LOGGER = LoggerFactory.getLogger(LineSplitProcessor.class.getName()); + /** Maps field names to their corresponding column indices */ private Map fieldsMapping; + + /** List of properties that should be overwritten during import */ private List propertiesToOverwrite; + + /** Property used for merging profiles */ private String mergingProperty; + + /** Whether to overwrite existing profiles during import */ private boolean overwriteExistingProfiles; + + /** Whether the CSV file contains a header row */ private boolean hasHeader; + + /** Whether the CSV file contains a column for delete operations */ private boolean hasDeleteColumn; + + /** Character used to separate columns in the CSV */ private String columnSeparator; + /** Character used to separate multiple values within a field */ private String multiValueSeparator; + + /** Characters used to delimit multi-value fields */ private String multiValueDelimiter; + /** Collection of property types used for type conversion */ private Collection profilePropertyTypes; + /** + * Processes a single line from a CSV file and converts it into a ProfileToImport object. + * + *

    The method performs the following operations: + *

      + *
    • Handles one-shot import configurations if present
    • + *
    • Skips header row if configured
    • + *
    • Parses CSV line using RFC4180 standard
    • + *
    • Validates field mapping against data
    • + *
    • Converts fields according to their property types
    • + *
    • Handles multi-value fields
    • + *
    • Sets up profile merging configuration
    • + *
    • Processes delete operations if configured
    • + *
    + *

    + * + * @param exchange the Camel exchange containing the CSV line to process + * @throws Exception if an error occurs during processing, including BadProfileDataFormatException + */ @Override public void process(Exchange exchange) throws Exception { diff --git a/extensions/router/router-core/src/main/java/org/apache/unomi/router/core/processor/UnomiStorageProcessor.java b/extensions/router/router-core/src/main/java/org/apache/unomi/router/core/processor/UnomiStorageProcessor.java index 94737b50fb..3caadc8789 100644 --- a/extensions/router/router-core/src/main/java/org/apache/unomi/router/core/processor/UnomiStorageProcessor.java +++ b/extensions/router/router-core/src/main/java/org/apache/unomi/router/core/processor/UnomiStorageProcessor.java @@ -28,13 +28,43 @@ import java.util.Set; /** - * Created by amidani on 29/12/2016. + * A Camel processor that handles the storage of imported profiles in the Unomi system. + * This processor is responsible for managing the final stage of profile import, including + * segment calculation and profile persistence. + * + *

    The processor performs the following operations: + *

      + *
    • Processes profiles marked for import
    • + *
    • Calculates segments and scores for non-deleted profiles
    • + *
    • Updates profile information with calculated segments
    • + *
    • Persists profiles in the Unomi storage system
    • + *
    + *

    + * + * @since 1.0 */ public class UnomiStorageProcessor implements Processor { + /** Service for handling profile import operations */ private ProfileImportService profileImportService; + + /** Service for managing profile segments and scoring */ private SegmentService segmentService; + /** + * Processes the exchange by storing or updating the profile in Unomi's storage system. + * + *

    This method: + *

      + *
    • Extracts the ProfileToImport from the message body
    • + *
    • For non-delete operations, calculates and updates segments and scores
    • + *
    • Persists the profile using the ProfileImportService
    • + *
    + *

    + * + * @param exchange the Camel exchange containing the profile to process + * @throws Exception if an error occurs during processing + */ @Override public void process(Exchange exchange) throws Exception { @@ -59,10 +89,20 @@ public void process(Exchange exchange) } } + /** + * Sets the profile import service used for persisting profiles. + * + * @param profileImportService the service responsible for profile import operations + */ public void setProfileImportService(ProfileImportService profileImportService) { this.profileImportService = profileImportService; } + /** + * Sets the segment service used for calculating profile segments and scores. + * + * @param segmentService the service responsible for segment calculations + */ public void setSegmentService(SegmentService segmentService) { this.segmentService = segmentService; } diff --git a/extensions/router/router-core/src/main/java/org/apache/unomi/router/core/route/ProfileExportCollectRouteBuilder.java b/extensions/router/router-core/src/main/java/org/apache/unomi/router/core/route/ProfileExportCollectRouteBuilder.java index 5529c109b8..3c690a36e5 100644 --- a/extensions/router/router-core/src/main/java/org/apache/unomi/router/core/route/ProfileExportCollectRouteBuilder.java +++ b/extensions/router/router-core/src/main/java/org/apache/unomi/router/core/route/ProfileExportCollectRouteBuilder.java @@ -31,19 +31,58 @@ import java.util.Map; /** - * Created by amidani on 27/06/2017. + * A Camel route builder that handles the collection of profiles for export. + * This route builder creates routes that periodically collect profiles based on + * segment criteria and prepare them for export processing. + * + *

    Features: + *

      + *
    • Timer-based profile collection
    • + *
    • Segment-based profile filtering
    • + *
    • Support for multiple export configurations
    • + *
    • Configurable collection intervals
    • + *
    • Security through endpoint allowlist
    • + *
    • Support for both Kafka and direct endpoints
    • + *
    + *

    + * + * @since 1.0 */ public class ProfileExportCollectRouteBuilder extends RouterAbstractRouteBuilder { private static final Logger LOGGER = LoggerFactory.getLogger(ProfileExportCollectRouteBuilder.class); + /** List of export configurations to process */ private List exportConfigurationList; + + /** Service for persisting and retrieving data */ private PersistenceService persistenceService; + /** + * Constructs a new route builder with Kafka configuration. + * + * @param kafkaProps map containing Kafka configuration properties + * @param configType the type of configuration (kafka/direct) + */ public ProfileExportCollectRouteBuilder(Map kafkaProps, String configType) { super(kafkaProps, configType); } + /** + * Configures the routes for collecting profiles to export. + * Creates a route for each export configuration that matches the criteria. + * + *

    Each route: + *

      + *
    • Runs on a configured timer schedule
    • + *
    • Collects profiles based on segment criteria
    • + *
    • Processes profiles for export
    • + *
    • Routes data to appropriate endpoints
    • + *
    + *

    + * + * @throws Exception if an error occurs during route configuration + */ @Override public void configure() throws Exception { if (exportConfigurationList == null || exportConfigurationList.isEmpty()) { @@ -93,10 +132,20 @@ public void configure() throws Exception { } } + /** + * Sets the list of export configurations to process. + * + * @param exportConfigurationList list of export configurations + */ public void setExportConfigurationList(List exportConfigurationList) { this.exportConfigurationList = exportConfigurationList; } + /** + * Sets the persistence service for data operations. + * + * @param persistenceService service for persisting and retrieving data + */ public void setPersistenceService(PersistenceService persistenceService) { this.persistenceService = persistenceService; } diff --git a/extensions/router/router-core/src/main/java/org/apache/unomi/router/core/route/ProfileExportProducerRouteBuilder.java b/extensions/router/router-core/src/main/java/org/apache/unomi/router/core/route/ProfileExportProducerRouteBuilder.java index e11378590c..cc903e84a1 100644 --- a/extensions/router/router-core/src/main/java/org/apache/unomi/router/core/route/ProfileExportProducerRouteBuilder.java +++ b/extensions/router/router-core/src/main/java/org/apache/unomi/router/core/route/ProfileExportProducerRouteBuilder.java @@ -29,24 +29,67 @@ import java.util.Map; /** - * Created by amidani on 28/06/2017. + * A Camel route builder that handles the production of export data from collected profiles. + * This route builder creates routes that process collected profiles and formats them + * for export to the configured destination. + * + *

    Features: + *

      + *
    • Profile data transformation to export format
    • + *
    • Line-by-line processing with aggregation
    • + *
    • Support for multiple export destinations
    • + *
    • Completion handling and status updates
    • + *
    • Support for both Kafka and direct endpoints
    • + *
    + *

    + * + * @since 1.0 */ public class ProfileExportProducerRouteBuilder extends RouterAbstractRouteBuilder { private static final Logger LOGGER = LoggerFactory.getLogger(ProfileExportProducerRouteBuilder.class); + /** Processor for handling export completion */ private ExportRouteCompletionProcessor exportRouteCompletionProcessor; + /** Service for profile export operations */ private ProfileExportService profileExportService; + /** + * Constructs a new route builder with Kafka configuration. + * + * @param kafkaProps map containing Kafka configuration properties + * @param configType the type of configuration (kafka/direct) + */ public ProfileExportProducerRouteBuilder(Map kafkaProps, String configType) { super(kafkaProps, configType); } + /** + * Sets the profile export service. + * + * @param profileExportService service for handling profile exports + */ public void setProfileExportService(ProfileExportService profileExportService) { this.profileExportService = profileExportService; } + /** + * Configures the routes for producing export data. + * Creates a route that processes collected profiles and prepares them for export. + * + *

    The route: + *

      + *
    • Unmarshals incoming profile data
    • + *
    • Processes profiles into export format
    • + *
    • Aggregates lines for batch processing
    • + *
    • Handles export completion
    • + *
    • Routes data to configured destinations
    • + *
    + *

    + * + * @throws Exception if an error occurs during route configuration + */ @Override public void configure() throws Exception { @@ -69,6 +112,11 @@ public void configure() throws Exception { } + /** + * Sets the processor for handling export completion. + * + * @param exportRouteCompletionProcessor processor for export completion handling + */ public void setExportRouteCompletionProcessor(ExportRouteCompletionProcessor exportRouteCompletionProcessor) { this.exportRouteCompletionProcessor = exportRouteCompletionProcessor; } diff --git a/extensions/router/router-core/src/main/java/org/apache/unomi/router/core/route/ProfileImportFromSourceRouteBuilder.java b/extensions/router/router-core/src/main/java/org/apache/unomi/router/core/route/ProfileImportFromSourceRouteBuilder.java index 1ebc5c3884..faff0cfb5c 100644 --- a/extensions/router/router-core/src/main/java/org/apache/unomi/router/core/route/ProfileImportFromSourceRouteBuilder.java +++ b/extensions/router/router-core/src/main/java/org/apache/unomi/router/core/route/ProfileImportFromSourceRouteBuilder.java @@ -37,20 +37,59 @@ import java.util.Map; /** - * Created by amidani on 26/04/2017. + * A Camel route builder that handles the import of profiles from configured sources. + * This route builder creates routes that process incoming profile data from various + * sources and prepares it for import into Unomi. + * + *

    Features: + *

      + *
    • Support for multiple import configurations
    • + *
    • Line-by-line processing of import data
    • + *
    • Error handling and failure reporting
    • + *
    • Configuration validation and status updates
    • + *
    • Support for both Kafka and direct endpoints
    • + *
    • Graceful shutdown handling
    • + *
    + *

    + * + * @since 1.0 */ - public class ProfileImportFromSourceRouteBuilder extends RouterAbstractRouteBuilder { private static final Logger LOGGER = LoggerFactory.getLogger(ProfileImportFromSourceRouteBuilder.class.getName()); + /** List of import configurations to process */ private List importConfigurationList; + + /** Service for managing import configurations */ private ImportExportConfigurationService importConfigurationService; + /** + * Constructs a new route builder with Kafka configuration. + * + * @param kafkaProps map containing Kafka configuration properties + * @param configType the type of configuration (kafka/direct) + */ public ProfileImportFromSourceRouteBuilder(Map kafkaProps, String configType) { super(kafkaProps, configType); } + /** + * Configures the routes for importing profiles from sources. + * Creates routes for each import configuration and sets up error handling. + * + *

    The routes: + *

      + *
    • Handle data validation and format errors
    • + *
    • Process data line by line
    • + *
    • Update import status and progress
    • + *
    • Route processed data to appropriate endpoints
    • + *
    • Manage graceful completion of imports
    • + *
    + *

    + * + * @throws Exception if an error occurs during route configuration + */ @Override public void configure() throws Exception { @@ -132,10 +171,20 @@ public void process(Exchange exchange) throws Exception { } } + /** + * Sets the list of import configurations to process. + * + * @param importConfigurationList list of import configurations + */ public void setImportConfigurationList(List importConfigurationList) { this.importConfigurationList = importConfigurationList; } + /** + * Sets the service for managing import configurations. + * + * @param importConfigurationService service for handling import configurations + */ public void setImportConfigurationService(ImportExportConfigurationService importConfigurationService) { this.importConfigurationService = importConfigurationService; } diff --git a/extensions/router/router-core/src/main/java/org/apache/unomi/router/core/route/ProfileImportOneShotRouteBuilder.java b/extensions/router/router-core/src/main/java/org/apache/unomi/router/core/route/ProfileImportOneShotRouteBuilder.java index 863437032e..6ac223f3d2 100644 --- a/extensions/router/router-core/src/main/java/org/apache/unomi/router/core/route/ProfileImportOneShotRouteBuilder.java +++ b/extensions/router/router-core/src/main/java/org/apache/unomi/router/core/route/ProfileImportOneShotRouteBuilder.java @@ -30,18 +30,59 @@ import java.util.Map; /** - * Created by amidani on 22/05/2017. + * A Camel route builder that handles one-time profile imports from files. + * This route builder creates routes that process CSV files dropped into a + * monitored directory for one-time import operations. + * + *

    Features: + *

      + *
    • File-based import processing
    • + *
    • Configuration lookup from filename
    • + *
    • CSV file processing with error handling
    • + *
    • Support for both Kafka and direct endpoints
    • + *
    • Automatic file movement after processing
    • + *
    • Error reporting and failed file handling
    • + *
    + *

    + * + * @since 1.0 */ public class ProfileImportOneShotRouteBuilder extends RouterAbstractRouteBuilder { private static final Logger LOGGER = LoggerFactory.getLogger(ProfileImportOneShotRouteBuilder.class.getName()); + + /** Processor for extracting import configuration from filenames */ private ImportConfigByFileNameProcessor importConfigByFileNameProcessor; + + /** Directory to monitor for import files */ private String uploadDir; + /** + * Constructs a new route builder with Kafka configuration. + * + * @param kafkaProps map containing Kafka configuration properties + * @param configType the type of configuration (kafka/direct) + */ public ProfileImportOneShotRouteBuilder(Map kafkaProps, String configType) { super(kafkaProps, configType); } + /** + * Configures the route for one-shot profile imports. + * Creates a route that monitors a directory for CSV files and processes them for import. + * + *

    The route: + *

      + *
    • Monitors upload directory for CSV files
    • + *
    • Extracts configuration from filename
    • + *
    • Processes file contents line by line
    • + *
    • Handles validation and format errors
    • + *
    • Routes processed data to appropriate endpoints
    • + *
    + *

    + * + * @throws Exception if an error occurs during route configuration + */ @Override public void configure() throws Exception { @@ -81,10 +122,20 @@ public void configure() throws Exception { } } + /** + * Sets the processor for handling import configuration by filename. + * + * @param importConfigByFileNameProcessor processor for filename-based configuration + */ public void setImportConfigByFileNameProcessor(ImportConfigByFileNameProcessor importConfigByFileNameProcessor) { this.importConfigByFileNameProcessor = importConfigByFileNameProcessor; } + /** + * Sets the directory to monitor for import files. + * + * @param uploadDir path to the directory to monitor + */ public void setUploadDir(String uploadDir) { this.uploadDir = uploadDir; } diff --git a/extensions/router/router-core/src/main/java/org/apache/unomi/router/core/route/ProfileImportToUnomiRouteBuilder.java b/extensions/router/router-core/src/main/java/org/apache/unomi/router/core/route/ProfileImportToUnomiRouteBuilder.java index ff4942c75a..44bebc1491 100644 --- a/extensions/router/router-core/src/main/java/org/apache/unomi/router/core/route/ProfileImportToUnomiRouteBuilder.java +++ b/extensions/router/router-core/src/main/java/org/apache/unomi/router/core/route/ProfileImportToUnomiRouteBuilder.java @@ -29,19 +29,56 @@ import java.util.Map; /** - * Created by amidani on 26/04/2017. + * A Camel route builder that handles the final stage of profile imports by storing + * processed profile data into Apache Unomi's storage system. + * + *

    Features: + *

      + *
    • Final processing of imported profiles
    • + *
    • Integration with Unomi's storage system
    • + *
    • Support for both Kafka and direct endpoints
    • + *
    • Import completion handling
    • + *
    • Error handling and reporting
    • + *
    + *

    + * + * @since 1.0 */ public class ProfileImportToUnomiRouteBuilder extends RouterAbstractRouteBuilder { private static final Logger LOGGER = LoggerFactory.getLogger(ProfileImportToUnomiRouteBuilder.class.getName()); + /** Processor for storing profiles in Unomi */ private UnomiStorageProcessor unomiStorageProcessor; + + /** Processor for handling import completion */ private ImportRouteCompletionProcessor importRouteCompletionProcessor; + /** + * Constructs a new route builder with Kafka configuration. + * + * @param kafkaProps map containing Kafka configuration properties + * @param configType the type of configuration (kafka/direct) + */ public ProfileImportToUnomiRouteBuilder(Map kafkaProps, String configType) { super(kafkaProps, configType); } + /** + * Configures the route for storing imported profiles in Unomi. + * Creates a route that processes incoming profile data and stores it in Unomi's storage system. + * + *

    The route: + *

      + *
    • Receives processed profile data
    • + *
    • Stores profiles in Unomi's storage system
    • + *
    • Handles import completion
    • + *
    • Manages error reporting
    • + *
    + *

    + * + * @throws Exception if an error occurs during route configuration + */ @Override public void configure() throws Exception { @@ -67,10 +104,20 @@ public void configure() throws Exception { .to("log:org.apache.unomi.router?level=DEBUG"); } + /** + * Sets the processor for storing profiles in Unomi. + * + * @param unomiStorageProcessor processor for Unomi storage operations + */ public void setUnomiStorageProcessor(UnomiStorageProcessor unomiStorageProcessor) { this.unomiStorageProcessor = unomiStorageProcessor; } + /** + * Sets the processor for handling import completion. + * + * @param importRouteCompletionProcessor processor for import completion operations + */ public void setImportRouteCompletionProcessor(ImportRouteCompletionProcessor importRouteCompletionProcessor) { this.importRouteCompletionProcessor = importRouteCompletionProcessor; } diff --git a/extensions/router/router-core/src/main/java/org/apache/unomi/router/core/route/RouterAbstractRouteBuilder.java b/extensions/router/router-core/src/main/java/org/apache/unomi/router/core/route/RouterAbstractRouteBuilder.java index ad06a00ecb..5ca80bfffc 100644 --- a/extensions/router/router-core/src/main/java/org/apache/unomi/router/core/route/RouterAbstractRouteBuilder.java +++ b/extensions/router/router-core/src/main/java/org/apache/unomi/router/core/route/RouterAbstractRouteBuilder.java @@ -28,26 +28,66 @@ import java.util.Map; /** - * Created by amidani on 13/06/2017. + * Abstract base class for all Unomi router route builders. + * This class provides common functionality and configuration for both import + * and export routes, supporting both Kafka and direct endpoint configurations. + * + *

    Features: + *

      + *
    • Common Kafka configuration handling
    • + *
    • Endpoint URI generation for both Kafka and direct modes
    • + *
    • Shared configuration for JSON data format
    • + *
    • Profile service integration
    • + *
    • Endpoint security through allowlist
    • + *
    + *

    + * + * @since 1.0 */ public abstract class RouterAbstractRouteBuilder extends RouteBuilder { + /** JSON data format configuration */ protected JacksonDataFormat jacksonDataFormat; + /** Kafka broker host */ protected String kafkaHost; + + /** Kafka broker port */ protected String kafkaPort; + + /** Topic for import operations */ protected String kafkaImportTopic; + + /** Topic for export operations */ protected String kafkaExportTopic; + + /** Consumer group ID for import operations */ protected String kafkaImportGroupId; + + /** Consumer group ID for export operations */ protected String kafkaExportGroupId; + + /** Number of Kafka consumers */ protected String kafkaConsumerCount; + + /** Auto-commit configuration for Kafka */ protected String kafkaAutoCommit; + /** Configuration type (kafka/direct) */ protected String configType; + + /** List of allowed endpoint schemes */ protected String allowedEndpoints; + /** Service for profile operations */ protected ProfileService profileService; + /** + * Constructs a new route builder with Kafka configuration. + * + * @param kafkaProps map containing Kafka configuration properties + * @param configType the type of configuration (kafka/direct) + */ public RouterAbstractRouteBuilder(Map kafkaProps, String configType) { this.kafkaHost = kafkaProps.get("kafkaHost"); this.kafkaPort = kafkaProps.get("kafkaPort"); @@ -60,6 +100,21 @@ public RouterAbstractRouteBuilder(Map kafkaProps, String configT this.configType = configType; } + /** + * Gets the appropriate endpoint URI based on configuration type and operation. + * + *

    This method: + *

      + *
    • Creates Kafka endpoints with appropriate configuration when using Kafka
    • + *
    • Returns direct endpoint URIs when not using Kafka
    • + *
    • Configures consumer properties for incoming endpoints
    • + *
    + *

    + * + * @param direction the direction of the endpoint (to/from) + * @param operationDepositBuffer the operation buffer identifier + * @return Object either a KafkaEndpoint or String depending on configuration + */ public Object getEndpointURI(String direction, String operationDepositBuffer) { Object endpoint; if (RouterConstants.CONFIG_TYPE_KAFKA.equals(configType)) { @@ -91,14 +146,29 @@ public Object getEndpointURI(String direction, String operationDepositBuffer) { return endpoint; } + /** + * Sets the JSON data format configuration. + * + * @param jacksonDataFormat the JSON data format to use + */ public void setJacksonDataFormat(JacksonDataFormat jacksonDataFormat) { this.jacksonDataFormat = jacksonDataFormat; } + /** + * Sets the list of allowed endpoint schemes. + * + * @param allowedEndpoints comma-separated list of allowed endpoint schemes + */ public void setAllowedEndpoints(String allowedEndpoints) { this.allowedEndpoints = allowedEndpoints; } + /** + * Sets the profile service. + * + * @param profileService the service for profile operations + */ public void setProfileService(ProfileService profileService) { this.profileService = profileService; } diff --git a/extensions/router/router-core/src/main/java/org/apache/unomi/router/core/strategy/ArrayListAggregationStrategy.java b/extensions/router/router-core/src/main/java/org/apache/unomi/router/core/strategy/ArrayListAggregationStrategy.java index ca87ad3bd3..c113e3aebd 100644 --- a/extensions/router/router-core/src/main/java/org/apache/unomi/router/core/strategy/ArrayListAggregationStrategy.java +++ b/extensions/router/router-core/src/main/java/org/apache/unomi/router/core/strategy/ArrayListAggregationStrategy.java @@ -22,11 +22,40 @@ import java.util.ArrayList; /** - * Created by amidani on 16/06/2017. + * An implementation of Camel's AggregationStrategy that aggregates exchange bodies into an ArrayList. + * This strategy is useful when you need to collect multiple messages into a single list for batch processing + * or grouped operations within the Unomi Router. + * + *

    The strategy maintains the following behavior: + *

      + *
    • For the first message (when oldExchange is null), it creates a new ArrayList and adds the message body to it
    • + *
    • For subsequent messages, it adds the new message body to the existing ArrayList
    • + *
    + *

    + * + *

    The ArrayList is maintained in the exchange body, allowing for easy access to all aggregated items + * once the aggregation is complete.

    + * + * @since 1.0 */ public class ArrayListAggregationStrategy implements AggregationStrategy { - + /** + * Aggregates exchange messages by collecting their bodies into an ArrayList. + * + *

    This method implements the core aggregation logic where: + *

      + *
    • The new exchange's body is extracted as is (maintaining its original type)
    • + *
    • If this is the first message, a new ArrayList is created to store the messages
    • + *
    • The new body is added to the ArrayList
    • + *
    • The ArrayList is maintained in the exchange body for subsequent aggregations
    • + *
    + *

    + * + * @param oldExchange the previous exchange being aggregated (may be null on first invocation) + * @param newExchange the current exchange being aggregated (contains the new item to add to the list) + * @return the aggregated exchange containing the ArrayList of all aggregated items + */ public Exchange aggregate(Exchange oldExchange, Exchange newExchange) { Object newBody = newExchange.getIn().getBody(); ArrayList list = null; diff --git a/extensions/router/router-core/src/main/java/org/apache/unomi/router/core/strategy/StringLinesAggregationStrategy.java b/extensions/router/router-core/src/main/java/org/apache/unomi/router/core/strategy/StringLinesAggregationStrategy.java index d01859f226..8ccabe6871 100644 --- a/extensions/router/router-core/src/main/java/org/apache/unomi/router/core/strategy/StringLinesAggregationStrategy.java +++ b/extensions/router/router-core/src/main/java/org/apache/unomi/router/core/strategy/StringLinesAggregationStrategy.java @@ -22,10 +22,40 @@ import org.apache.unomi.router.api.RouterUtils; /** - * Created by amidani on 29/06/2017. + * An implementation of Camel's AggregationStrategy that combines multiple text lines into a single string + * for export purposes. This strategy is specifically designed to work with the Unomi Router's export functionality, + * where multiple data lines need to be aggregated into a single export file. + * + *

    The strategy maintains the following behavior: + *

      + *
    • For the first message (when oldExchange is null), it simply returns the new exchange
    • + *
    • For subsequent messages, it appends the new content to the existing content using the configured line separator
    • + *
    + *

    + * + *

    The line separator used for aggregation is obtained from the ExportConfiguration object + * stored in the exchange header under the key "exportConfig".

    + * + * @since 1.0 */ public class StringLinesAggregationStrategy implements AggregationStrategy { + /** + * Aggregates two exchanges by combining their body content with appropriate line separation. + * + *

    This method implements the core aggregation logic where: + *

      + *
    • The new exchange's body is extracted as a String
    • + *
    • The line separator is obtained from the export configuration in the exchange header
    • + *
    • If there's an old exchange, the new content is appended to it with the line separator
    • + *
    • If there's no old exchange, the new exchange is returned as is
    • + *
    + *

    + * + * @param oldExchange the previous exchange being aggregated (may be null on first invocation) + * @param newExchange the current exchange being aggregated (contains the new line to append) + * @return the aggregated exchange containing the combined content + */ public Exchange aggregate(Exchange oldExchange, Exchange newExchange) { Object newBody = newExchange.getIn().getBody(String.class); String lineSeparator = newExchange.getIn().getHeader("exportConfig", ExportConfiguration.class).getLineSeparator(); From dd35f3e44d831b2e16f74a85da63b266df13b44e Mon Sep 17 00:00:00 2001 From: Serge Huber Date: Fri, 15 May 2026 08:10:13 +0200 Subject: [PATCH 04/21] Add manual dispatch to CodeQL Java workflow --- .github/workflows/codeql-analysis-java.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/codeql-analysis-java.yml b/.github/workflows/codeql-analysis-java.yml index 0b8b7c4278..c823fc4720 100644 --- a/.github/workflows/codeql-analysis-java.yml +++ b/.github/workflows/codeql-analysis-java.yml @@ -17,6 +17,7 @@ on: pull_request: # The branches below must be a subset of the branches above branches: [ master ] + workflow_dispatch: schedule: - cron: '38 1 * * 0' From 86d14d0cbe3db0d80cdd485e52935fc61513c58e Mon Sep 17 00:00:00 2001 From: Serge Huber Date: Fri, 15 May 2026 08:10:25 +0200 Subject: [PATCH 05/21] Add manual dispatch to CodeQL JavaScript workflow --- .github/workflows/codeql-analysis-javascript.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/codeql-analysis-javascript.yml b/.github/workflows/codeql-analysis-javascript.yml index 542b973b65..39efa7cbe7 100644 --- a/.github/workflows/codeql-analysis-javascript.yml +++ b/.github/workflows/codeql-analysis-javascript.yml @@ -17,6 +17,7 @@ on: pull_request: # The branches below must be a subset of the branches above branches: [ master ] + workflow_dispatch: schedule: - cron: '38 1 * * 0' From 124af7eaa96213ba3b0c7af4de06c46c72e39d0c Mon Sep 17 00:00:00 2001 From: Serge Huber Date: Fri, 15 May 2026 08:13:26 +0200 Subject: [PATCH 06/21] Add manual dispatch to CodeQL Java workflow --- .github/workflows/codeql-analysis-java.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/codeql-analysis-java.yml b/.github/workflows/codeql-analysis-java.yml index 0b8b7c4278..c823fc4720 100644 --- a/.github/workflows/codeql-analysis-java.yml +++ b/.github/workflows/codeql-analysis-java.yml @@ -17,6 +17,7 @@ on: pull_request: # The branches below must be a subset of the branches above branches: [ master ] + workflow_dispatch: schedule: - cron: '38 1 * * 0' From c5b167c295acd2b04c25dd12f7ca3f107f8970d5 Mon Sep 17 00:00:00 2001 From: Serge Huber Date: Fri, 15 May 2026 08:14:21 +0200 Subject: [PATCH 07/21] Add manual dispatch to CodeQL JavaScript workflow --- .github/workflows/codeql-analysis-javascript.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/codeql-analysis-javascript.yml b/.github/workflows/codeql-analysis-javascript.yml index 542b973b65..39efa7cbe7 100644 --- a/.github/workflows/codeql-analysis-javascript.yml +++ b/.github/workflows/codeql-analysis-javascript.yml @@ -17,6 +17,7 @@ on: pull_request: # The branches below must be a subset of the branches above branches: [ master ] + workflow_dispatch: schedule: - cron: '38 1 * * 0' From 7261e6ac5f1b32628e09cea64299a227698c3f30 Mon Sep 17 00:00:00 2001 From: Serge Huber Date: Fri, 15 May 2026 08:14:49 +0200 Subject: [PATCH 08/21] Add manual dispatch to CodeQL Java workflow --- .github/workflows/codeql-analysis-java.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/codeql-analysis-java.yml b/.github/workflows/codeql-analysis-java.yml index 0b8b7c4278..c823fc4720 100644 --- a/.github/workflows/codeql-analysis-java.yml +++ b/.github/workflows/codeql-analysis-java.yml @@ -17,6 +17,7 @@ on: pull_request: # The branches below must be a subset of the branches above branches: [ master ] + workflow_dispatch: schedule: - cron: '38 1 * * 0' From 624b027742a59cd33f5ceb3bcf565d2ddd485c84 Mon Sep 17 00:00:00 2001 From: Serge Huber Date: Fri, 15 May 2026 08:15:14 +0200 Subject: [PATCH 09/21] Add manual dispatch to CodeQL JavaScript workflow --- .github/workflows/codeql-analysis-javascript.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/codeql-analysis-javascript.yml b/.github/workflows/codeql-analysis-javascript.yml index 542b973b65..39efa7cbe7 100644 --- a/.github/workflows/codeql-analysis-javascript.yml +++ b/.github/workflows/codeql-analysis-javascript.yml @@ -17,6 +17,7 @@ on: pull_request: # The branches below must be a subset of the branches above branches: [ master ] + workflow_dispatch: schedule: - cron: '38 1 * * 0' From 22de38c24b4e693f4859997caa800ccbc232c6fa Mon Sep 17 00:00:00 2001 From: Serge Huber Date: Fri, 15 May 2026 19:40:22 +0200 Subject: [PATCH 10/21] CI: always publish IT JUnit report and update check on re-run Run mikepenz/action-junit-report after every integration matrix leg (if: always), with update_check and per-matrix check_name, so a green re-run replaces the stale red JUnit check. Use fail_on_failure=false and require_tests=false; continue-on-error so missing XML cannot fail the job. --- .github/workflows/unomi-ci-build-tests.yml | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/.github/workflows/unomi-ci-build-tests.yml b/.github/workflows/unomi-ci-build-tests.yml index e629253cd6..4cb248c54a 100644 --- a/.github/workflows/unomi-ci-build-tests.yml +++ b/.github/workflows/unomi-ci-build-tests.yml @@ -79,8 +79,15 @@ jobs: itests/target/exam/**/data/log itests/target/elasticsearch0/data itests/target/elasticsearch0/logs + # Always publish so a later "re-run failed jobs" pass updates the check to green. + # Previously `if: failure()` left a stale red "JUnit Test Report" when ITs passed on re-run. - name: Publish Test Report uses: mikepenz/action-junit-report@v3 - if: failure() + if: always() + continue-on-error: true with: report_paths: 'itests/target/failsafe-reports/TEST-*.xml' + check_name: 'JUnit Test Report (${{ matrix.search-engine }})' + update_check: true + fail_on_failure: false + require_tests: false From bcf5a68989dfa2a93206a50af6fe598d24920212 Mon Sep 17 00:00:00 2001 From: Serge Huber Date: Sat, 16 May 2026 08:33:02 +0200 Subject: [PATCH 11/21] UNOMI-920: Address PR review feedback for YAML toString support Use identity-based visited sets for circular-reference detection, harden Condition deepCopy and parameter handling, fix Parameter max-depth YAML, wire slf4j.version in unomi-api, and expand unit test coverage. --- api/pom.xml | 2 + .../main/java/org/apache/unomi/api/Item.java | 5 +- .../java/org/apache/unomi/api/Metadata.java | 3 +- .../org/apache/unomi/api/MetadataItem.java | 3 +- .../java/org/apache/unomi/api/Parameter.java | 11 +- .../org/apache/unomi/api/actions/Action.java | 3 +- .../apache/unomi/api/actions/ActionType.java | 3 +- .../unomi/api/conditions/Condition.java | 95 ++++++---- .../unomi/api/conditions/ConditionType.java | 3 +- .../java/org/apache/unomi/api/goals/Goal.java | 4 +- .../java/org/apache/unomi/api/rules/Rule.java | 3 +- .../apache/unomi/api/segments/Scoring.java | 3 +- .../unomi/api/segments/ScoringElement.java | 3 +- .../apache/unomi/api/segments/Segment.java | 4 +- .../org/apache/unomi/api/utils/YamlUtils.java | 17 +- .../org/apache/unomi/api/ParameterTest.java | 81 +++++++++ .../unomi/api/conditions/ConditionTest.java | 171 ++++++++++++++++++ .../apache/unomi/api/utils/YamlUtilsTest.java | 145 +++++++++++++-- pom.xml | 1 + 19 files changed, 485 insertions(+), 75 deletions(-) create mode 100644 api/src/test/java/org/apache/unomi/api/ParameterTest.java create mode 100644 api/src/test/java/org/apache/unomi/api/conditions/ConditionTest.java diff --git a/api/pom.xml b/api/pom.xml index 4d9ed7fbd0..89d7d56add 100644 --- a/api/pom.xml +++ b/api/pom.xml @@ -66,6 +66,7 @@ org.slf4j slf4j-api + ${slf4j.version} provided @@ -93,6 +94,7 @@ org.slf4j slf4j-simple + ${slf4j.version} test 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 9836cf40df..4828025885 100644 --- a/api/src/main/java/org/apache/unomi/api/Item.java +++ b/api/src/main/java/org/apache/unomi/api/Item.java @@ -43,6 +43,9 @@ public abstract class Item implements Serializable, YamlConvertible { private static final Logger LOGGER = LoggerFactory.getLogger(Item.class.getName()); + /** + * Java serialization version; Unomi does not rely on Java serialization of this type as a cross-version persistence contract. + */ private static final long serialVersionUID = 1217180125083162915L; private static final Map itemTypeCache = new ConcurrentHashMap<>(); @@ -171,7 +174,7 @@ public Map toYaml(Set visited, int maxDepth) { .put("systemMetadata", "") .build(); } - final Set visitedSet = visited != null ? visited : new HashSet<>(); + final Set visitedSet = visited != null ? visited : YamlUtils.newIdentityVisitedSet(); // 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, 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 ec25c7fa89..4f650589f2 100644 --- a/api/src/main/java/org/apache/unomi/api/Metadata.java +++ b/api/src/main/java/org/apache/unomi/api/Metadata.java @@ -22,7 +22,6 @@ 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; @@ -299,7 +298,7 @@ public Map toYaml(Set visited, int maxDepth) { if (visited != null && visited.contains(this)) { return circularRef(); } - final Set visitedSet = visited != null ? visited : new HashSet<>(); + final Set visitedSet = visited != null ? visited : YamlUtils.newIdentityVisitedSet(); visitedSet.add(this); try { return YamlMapBuilder.create() 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 35ef596a25..68cf3b3248 100644 --- a/api/src/main/java/org/apache/unomi/api/MetadataItem.java +++ b/api/src/main/java/org/apache/unomi/api/MetadataItem.java @@ -22,7 +22,6 @@ import javax.xml.bind.annotation.XmlElement; import javax.xml.bind.annotation.XmlTransient; -import java.util.HashSet; import java.util.Map; import java.util.Set; @@ -83,7 +82,7 @@ public Map toYaml(Set visited, int maxDepth) { .put("metadata", "") .build(); } - final Set visitedSet = visited != null ? visited : new HashSet<>(); + final Set visitedSet = visited != null ? visited : YamlUtils.newIdentityVisitedSet(); // 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); 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 425d86280c..7fc7c7453b 100644 --- a/api/src/main/java/org/apache/unomi/api/Parameter.java +++ b/api/src/main/java/org/apache/unomi/api/Parameter.java @@ -24,14 +24,15 @@ 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, YamlConvertible { + /** + * Java serialization version; Unomi does not rely on Java serialization of this type as a cross-version persistence contract. + */ private static final long serialVersionUID = 6019392686888941547L; private String id; @@ -100,8 +101,10 @@ public void setDefaultValue(Object defaultValue) { public Map toYaml(Set visited, int maxDepth) { if (maxDepth <= 0) { return YamlUtils.YamlMapBuilder.create() - .put("id", id) - .put("validation", "") + .putIfNotNull("id", id) + .putIfNotNull("type", type) + .putIf("multivalued", true, multivalued) + .putIfNotNull("defaultValue", "") .build(); } return YamlUtils.YamlMapBuilder.create() 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 29c6927e1c..aeaba1bc6f 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 @@ -26,7 +26,6 @@ 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; @@ -143,7 +142,7 @@ public Map toYaml(Set visited, int maxDepth) { if (visited != null && visited.contains(this)) { return circularRef(); } - final Set visitedSet = visited != null ? visited : new HashSet<>(); + final Set visitedSet = visited != null ? visited : YamlUtils.newIdentityVisitedSet(); visitedSet.add(this); try { YamlMapBuilder builder = YamlMapBuilder.create() 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 e170c779b5..5da9493496 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 @@ -20,6 +20,7 @@ import org.apache.unomi.api.Metadata; import org.apache.unomi.api.MetadataItem; import org.apache.unomi.api.Parameter; +import org.apache.unomi.api.utils.YamlUtils; import org.apache.unomi.api.utils.YamlUtils.YamlConvertible; import org.apache.unomi.api.utils.YamlUtils.YamlMapBuilder; @@ -123,7 +124,7 @@ public Map toYaml(Set visited, int maxDepth) { if (visited != null && visited.contains(this)) { return circularRef(); } - final Set visitedSet = visited != null ? visited : new HashSet<>(); + final Set visitedSet = visited != null ? visited : YamlUtils.newIdentityVisitedSet(); visitedSet.add(this); try { return YamlMapBuilder.create() 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 812ee58628..6dc1b90474 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 @@ -24,7 +24,13 @@ import javax.xml.bind.annotation.XmlElement; import javax.xml.bind.annotation.XmlTransient; import java.io.Serializable; -import java.util.*; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.IdentityHashMap; +import java.util.List; +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; @@ -111,7 +117,7 @@ public Map getParameterValues() { * @param parameterValues a Map containing the parameter name - value pairs for this profile */ public void setParameterValues(Map parameterValues) { - this.parameterValues = parameterValues; + this.parameterValues = parameterValues != null ? parameterValues : new HashMap<>(); } /** @@ -121,7 +127,7 @@ public void setParameterValues(Map parameterValues) { * @return {@code true} if this condition contains a parameter with the specified name, {@code false} otherwise */ public boolean containsParameter(String name) { - return parameterValues.containsKey(name); + return parameterValues != null && parameterValues.containsKey(name); } /** @@ -142,6 +148,9 @@ public Object getParameter(String name) { * @param value the value of the parameter */ public void setParameter(String name, Object value) { + if (parameterValues == null) { + parameterValues = new HashMap<>(); + } parameterValues.put(name, value); } @@ -184,7 +193,7 @@ public Map toYaml(Set visited, int maxDepth) { if (visited != null && visited.contains(this)) { return circularRef(); } - final Set visitedSet = visited != null ? visited : new HashSet<>(); + final Set visitedSet = visited != null ? visited : YamlUtils.newIdentityVisitedSet(); visitedSet.add(this); try { YamlMapBuilder builder = YamlMapBuilder.create() @@ -203,52 +212,58 @@ public Map toYaml(Set visited, int maxDepth) { * Recursively copies all nested conditions to avoid sharing references. * * @return a deep copy of this condition + * @throws IllegalStateException if the condition graph contains a cycle through nested {@link Condition} values */ public Condition deepCopy() { - Condition copied = new Condition(); - if (this.conditionType != null) { - copied.setConditionType(this.conditionType); - } else if (this.conditionTypeId != null) { - copied.setConditionTypeId(this.conditionTypeId); + return deepCopy(new IdentityHashMap<>()); + } + + private Condition deepCopy(IdentityHashMap copying) { + if (copying.put(this, Boolean.TRUE) != null) { + throw new IllegalStateException("Cyclic Condition graph: cannot deepCopy()"); } + try { + Condition copied = new Condition(); + if (this.conditionType != null) { + copied.setConditionType(this.conditionType); + } else if (this.conditionTypeId != null) { + copied.setConditionTypeId(this.conditionTypeId); + } - // Deep copy parameter values - Map copiedParams = new HashMap<>(); - if (this.parameterValues != null) { - for (Map.Entry 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 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()); + // Deep copy parameter values + Map copiedParams = new HashMap<>(); + if (this.parameterValues != null) { + for (Map.Entry entry : this.parameterValues.entrySet()) { + Object value = entry.getValue(); + if (value instanceof Condition) { + copiedParams.put(entry.getKey(), ((Condition) value).deepCopy(copying)); + } else if (value instanceof Collection) { + Collection collection = (Collection) value; + Collection copiedCollection; + if (collection instanceof List) { + copiedCollection = new ArrayList<>(); } else { - // Not a condition, add as-is (for non-condition values in collections) - copiedCollection.add(item); + copiedCollection = new ArrayList<>(); + } + for (Object item : collection) { + if (item instanceof Condition) { + copiedCollection.add(((Condition) item).deepCopy(copying)); + } else { + copiedCollection.add(item); + } } + copiedParams.put(entry.getKey(), copiedCollection); + } else { + copiedParams.put(entry.getKey(), value); } - copiedParams.put(entry.getKey(), copiedCollection); - } else { - // Primitive or other non-condition value, copy as-is - copiedParams.put(entry.getKey(), value); } } - } - copied.setParameterValues(copiedParams); + copied.setParameterValues(copiedParams); - return copied; + return copied; + } finally { + copying.remove(this); + } } @Override 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 e8af9c41fc..3d22c00a3c 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 @@ -20,6 +20,7 @@ import org.apache.unomi.api.Metadata; import org.apache.unomi.api.MetadataItem; import org.apache.unomi.api.Parameter; +import org.apache.unomi.api.utils.YamlUtils; import org.apache.unomi.api.utils.YamlUtils.YamlConvertible; import org.apache.unomi.api.utils.YamlUtils.YamlMapBuilder; @@ -165,7 +166,7 @@ public Map toYaml(Set visited, int maxDepth) { if (visited != null && visited.contains(this)) { return circularRef(); } - final Set visitedSet = visited != null ? visited : new HashSet<>(); + final Set visitedSet = visited != null ? visited : YamlUtils.newIdentityVisitedSet(); visitedSet.add(this); try { return YamlMapBuilder.create() 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 12694b3d7e..d2cac5db85 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,10 +21,10 @@ 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; 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; @@ -116,7 +116,7 @@ public Map toYaml(Set visited, int maxDepth) { if (visited != null && visited.contains(this)) { return circularRef(); } - final Set visitedSet = visited != null ? visited : new HashSet<>(); + final Set visitedSet = visited != null ? visited : YamlUtils.newIdentityVisitedSet(); visitedSet.add(this); try { return YamlMapBuilder.create() 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 63750c0d44..cd6f751e5d 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,6 +20,7 @@ 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; import org.apache.unomi.api.utils.YamlUtils.YamlConvertible; import org.apache.unomi.api.utils.YamlUtils.YamlMapBuilder; @@ -222,7 +223,7 @@ public Map toYaml(Set visited, int maxDepth) { if (visited != null && visited.contains(this)) { return circularRef(); } - final Set visitedSet = visited != null ? visited : new HashSet<>(); + final Set visitedSet = visited != null ? visited : YamlUtils.newIdentityVisitedSet(); visitedSet.add(this); try { return YamlMapBuilder.create() 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 3d82e6e7e0..b32aa8ab08 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,6 +21,7 @@ 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; import org.apache.unomi.api.utils.YamlUtils.YamlConvertible; import org.apache.unomi.api.utils.YamlUtils.YamlMapBuilder; @@ -93,7 +94,7 @@ public Map toYaml(Set visited, int maxDepth) { if (visited != null && visited.contains(this)) { return circularRef(); } - final Set visitedSet = visited != null ? visited : new HashSet<>(); + final Set visitedSet = visited != null ? visited : YamlUtils.newIdentityVisitedSet(); visitedSet.add(this); try { return YamlMapBuilder.create() 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 bfc5a21ccb..ab79d132ac 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 @@ -23,7 +23,6 @@ import org.apache.unomi.api.utils.YamlUtils.YamlMapBuilder; import java.io.Serializable; -import java.util.HashSet; import java.util.Map; import java.util.Set; @@ -97,7 +96,7 @@ public Map toYaml(Set visited, int maxDepth) { if (visited != null && visited.contains(this)) { return circularRef(); } - final Set visitedSet = visited != null ? visited : new HashSet<>(); + final Set visitedSet = visited != null ? visited : YamlUtils.newIdentityVisitedSet(); visitedSet.add(this); try { return YamlMapBuilder.create() 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 2729ec74d0..126279c15f 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,10 +22,10 @@ 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; 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; @@ -98,7 +98,7 @@ public Map toYaml(Set visited, int maxDepth) { if (visited != null && visited.contains(this)) { return circularRef(); } - final Set visitedSet = visited != null ? visited : new HashSet<>(); + final Set visitedSet = visited != null ? visited : YamlUtils.newIdentityVisitedSet(); visitedSet.add(this); try { return YamlMapBuilder.create() 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 664f63fb79..a3dcd3cd09 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 @@ -20,6 +20,8 @@ import org.yaml.snakeyaml.DumperOptions; import org.yaml.snakeyaml.Yaml; +import java.util.Collections; +import java.util.IdentityHashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -53,7 +55,9 @@ public interface YamlConvertible { * 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 visited optional set of visited objects to detect circular references (may be null). + * When non-null, use identity semantics (e.g. {@link YamlUtils#newIdentityVisitedSet()}) + * so cycles are detected by object identity, not {@code equals}. * @param maxDepth maximum recursion depth (prevents StackOverflowError from deep nesting) * @return a Map representation of this object */ @@ -236,6 +240,17 @@ public static > List setToSortedList(Set set, F return set.stream().map(mapper).sorted().collect(Collectors.toList()); } + /** + * Creates an empty {@link Set} suitable for {@link YamlConvertible#toYaml(Set, int)} visited tracking. + * The set uses reference identity ({@link IdentityHashMap}), not {@link Object#equals(Object) equals}, + * so distinct object graphs are not mistaken for cycles when types override equality. + * + * @return a new modifiable identity-based set + */ + public static Set newIdentityVisitedSet() { + return Collections.newSetFromMap(new IdentityHashMap<>()); + } + /** * Converts a value to YAML-compatible format, handling nested structures. * For objects that implement YamlConvertible, circular reference detection is diff --git a/api/src/test/java/org/apache/unomi/api/ParameterTest.java b/api/src/test/java/org/apache/unomi/api/ParameterTest.java new file mode 100644 index 0000000000..2ed12d0c68 --- /dev/null +++ b/api/src/test/java/org/apache/unomi/api/ParameterTest.java @@ -0,0 +1,81 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.unomi.api; + +import org.junit.Test; + +import java.util.Map; + +import static org.junit.Assert.*; + +/** + * Unit tests for {@link Parameter} YAML and accessors. + */ +public class ParameterTest { + + @Test + public void testToYamlMaxDepthZeroIncludesExpectedKeys() { + Parameter p = new Parameter(); + p.setId("pid"); + p.setType("string"); + p.setMultivalued(true); + p.setDefaultValue("def"); + Map y = p.toYaml(null, 0); + assertEquals("pid", y.get("id")); + assertEquals("string", y.get("type")); + assertTrue(y.containsKey("multivalued")); + assertEquals("", y.get("defaultValue")); + } + + @Test + public void testToYamlMaxDepthZeroAddsDefaultValueTruncationMarkerEvenWhenUnset() { + Parameter p = new Parameter(); + p.setMultivalued(false); + Map y = p.toYaml(null, 0); + assertFalse(y.containsKey("id")); + assertFalse(y.containsKey("type")); + assertFalse(y.containsKey("multivalued")); + assertEquals("", y.get("defaultValue")); + } + + @Test + public void testToYamlNormalPath() { + Parameter p = new Parameter("id1", "number", false); + p.setDefaultValue(42); + Map y = p.toYaml(null, 10); + assertEquals("id1", y.get("id")); + assertEquals("number", y.get("type")); + assertFalse(y.containsKey("multivalued")); + assertEquals(42, y.get("defaultValue")); + } + + @Test + public void testToYamlMultivaluedTrueAddsFlag() { + Parameter p = new Parameter("i", "t", true); + Map y = p.toYaml(null, 10); + assertEquals(Boolean.TRUE, y.get("multivalued")); + } + + @Test + public void testToStringIsNonEmptyYaml() { + Parameter p = new Parameter("x", "boolean", false); + String s = p.toString(); + assertNotNull(s); + assertTrue(s.length() > 0); + assertTrue(s.contains("x")); + } +} diff --git a/api/src/test/java/org/apache/unomi/api/conditions/ConditionTest.java b/api/src/test/java/org/apache/unomi/api/conditions/ConditionTest.java new file mode 100644 index 0000000000..35e6edf250 --- /dev/null +++ b/api/src/test/java/org/apache/unomi/api/conditions/ConditionTest.java @@ -0,0 +1,171 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.unomi.api.conditions; + +import org.apache.unomi.api.Metadata; +import org.apache.unomi.api.utils.YamlUtils; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; + +import static org.junit.Assert.*; + +/** + * Unit tests for {@link Condition} behavior (parameters, YAML, deep copy). + */ +public class ConditionTest { + + @Test + public void testSetParameterValuesNullReplacesWithEmptyMap() { + Condition c = new Condition(); + c.setConditionTypeId("t"); + c.getParameterValues().put("k", "v"); + c.setParameterValues(null); + assertNotNull(c.getParameterValues()); + assertTrue(c.getParameterValues().isEmpty()); + assertFalse(c.containsParameter("k")); + assertNull(c.getParameter("k")); + } + + @Test + public void testSetParameterAfterClearingParameterValues() { + Condition c = new Condition(); + c.setConditionTypeId("t"); + c.setParameterValues(null); + c.setParameter("x", 1); + assertEquals(Integer.valueOf(1), c.getParameter("x")); + assertTrue(c.containsParameter("x")); + } + + @Test + public void testToYamlMaxDepthZeroUsesPlaceholder() { + Condition c = new Condition(); + c.setConditionTypeId("myType"); + c.getParameterValues().put("p", "v"); + Map y = c.toYaml(null, 0); + assertEquals("myType", y.get("type")); + assertEquals("", y.get("parameterValues")); + } + + @Test + public void testToYamlMaxDepthZeroDefaultTypeWhenIdMissing() { + Condition c = new Condition(); + Map y = c.toYaml(null, 0); + assertEquals("Condition", y.get("type")); + } + + @Test + public void testToYamlWhenAlreadyVisitedReturnsCircularMarker() { + Condition c = new Condition(); + c.setConditionTypeId("t"); + Set visited = YamlUtils.newIdentityVisitedSet(); + visited.add(c); + Map y = c.toYaml(visited, 5); + assertEquals("circular", y.get("$ref")); + } + + @Test + public void testToYamlOmitsParameterValuesWhenEmpty() { + Condition c = new Condition(); + c.setConditionTypeId("onlyType"); + Map y = c.toYaml(null, 10); + assertFalse(y.containsKey("parameterValues")); + } + + @Test + public void testToStringUsesYamlFormat() { + Condition c = new Condition(); + c.setConditionTypeId("ctype"); + String s = c.toString(); + assertNotNull(s); + assertTrue(s.contains("ctype")); + } + + @Test + public void testDeepCopyPreservesConditionTypeIdOnly() { + Condition c = new Condition(); + c.setConditionTypeId("idOnly"); + Condition copy = c.deepCopy(); + assertNotSame(c, copy); + assertEquals("idOnly", copy.getConditionTypeId()); + assertNull(copy.getConditionType()); + } + + @Test + public void testDeepCopyPreservesConditionTypeReference() { + ConditionType ct = new ConditionType(new Metadata("meta-ct")); + ct.setItemId("evaluatorType"); + Condition c = new Condition(ct); + Condition copy = c.deepCopy(); + assertSame(ct, copy.getConditionType()); + assertEquals("evaluatorType", copy.getConditionTypeId()); + } + + @Test + public void testDeepCopyNestedConditionInSetBecomesArrayList() { + Condition inner = new Condition(); + inner.setConditionTypeId("inner"); + Condition outer = new Condition(); + outer.setConditionTypeId("outer"); + Set nested = new LinkedHashSet<>(); + nested.add(inner); + outer.getParameterValues().put("conds", nested); + + Condition copy = outer.deepCopy(); + Object copiedVal = copy.getParameterValues().get("conds"); + assertTrue(copiedVal instanceof ArrayList); + @SuppressWarnings("unchecked") + Collection col = (Collection) copiedVal; + assertEquals(1, col.size()); + Condition copyInner = col.iterator().next(); + assertNotSame(inner, copyInner); + assertEquals("inner", copyInner.getConditionTypeId()); + } + + @Test(expected = IllegalStateException.class) + public void testDeepCopyRejectsSelfReferenceInParameterMap() { + Condition c = new Condition(); + c.setConditionTypeId("self"); + c.getParameterValues().put("me", c); + c.deepCopy(); + } + + @Test(expected = IllegalStateException.class) + public void testDeepCopyRejectsSelfInSingletonCollection() { + Condition c = new Condition(); + c.setConditionTypeId("self"); + c.getParameterValues().put("list", Collections.singletonList(c)); + c.deepCopy(); + } + + @Test + public void testEqualsAndHashCode() { + Condition a = new Condition(); + a.setConditionTypeId("t"); + a.getParameterValues().put("k", 1); + Condition b = new Condition(); + b.setConditionTypeId("t"); + b.getParameterValues().put("k", 1); + assertEquals(a, b); + assertEquals(a.hashCode(), b.hashCode()); + } +} 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 c40c066ca6..af41505121 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 @@ -198,7 +198,7 @@ public void testToYamlValueWithYamlConvertible() { map.put("test", "value"); return map; }; - Set visited = new HashSet<>(); + Set visited = YamlUtils.newIdentityVisitedSet(); Object result = YamlUtils.toYamlValue(convertible, visited); assertTrue("YamlConvertible should be converted to Map", result instanceof Map); Map map = (Map) result; @@ -208,7 +208,7 @@ public void testToYamlValueWithYamlConvertible() { @Test public void testToYamlValueWithList() { List list = Arrays.asList("a", "b", "c"); - Set visited = new HashSet<>(); + Set visited = YamlUtils.newIdentityVisitedSet(); Object result = YamlUtils.toYamlValue(list, visited); assertTrue("List should remain a List", result instanceof List); assertEquals("List should be unchanged", list, result); @@ -218,7 +218,7 @@ public void testToYamlValueWithList() { public void testToYamlValueWithMap() { Map map = new LinkedHashMap<>(); map.put("key", "value"); - Set visited = new HashSet<>(); + Set visited = YamlUtils.newIdentityVisitedSet(); Object result = YamlUtils.toYamlValue(map, visited); assertTrue("Map should remain a Map", result instanceof Map); assertEquals("Map should contain key-value", "value", ((Map) result).get("key")); @@ -226,14 +226,14 @@ public void testToYamlValueWithMap() { @Test public void testToYamlValueWithNull() { - Set visited = new HashSet<>(); + Set visited = YamlUtils.newIdentityVisitedSet(); Object result = YamlUtils.toYamlValue(null, visited); assertNull("Null should return null", result); } @Test public void testToYamlValueWithPrimitive() { - Set visited = new HashSet<>(); + Set visited = YamlUtils.newIdentityVisitedSet(); Object result = YamlUtils.toYamlValue(42, visited); assertEquals("Primitive should remain unchanged", 42, result); } @@ -446,12 +446,12 @@ public void testCircularReferenceInNestedMap() { Condition condition = new Condition(); condition.setConditionTypeId("testCondition"); // Nested map containing the rule + Map level2 = new HashMap<>(); + level2.put("rule", rule); + Map level1 = new HashMap<>(); + level1.put("level2", level2); Map nestedMap = new HashMap<>(); - nestedMap.put("level1", new HashMap() {{ - put("level2", new HashMap() {{ - put("rule", rule); - }}); - }}); + nestedMap.put("level1", level1); condition.getParameterValues().put("nested", nestedMap); rule.setCondition(condition); @@ -459,9 +459,9 @@ public void testCircularReferenceInNestedMap() { Map conditionMap = (Map) result.get("condition"); Map paramValues = (Map) conditionMap.get("parameterValues"); Map nested = (Map) paramValues.get("nested"); - Map level1 = (Map) nested.get("level1"); - Map level2 = (Map) level1.get("level2"); - Map circularRef = (Map) level2.get("rule"); + Map nestedLevel1 = (Map) nested.get("level1"); + Map nestedLevel2 = (Map) nestedLevel1.get("level2"); + Map circularRef = (Map) nestedLevel2.get("rule"); assertNotNull("Circular reference should be detected in nested map", circularRef); assertEquals("Should contain circular reference marker", "circular", circularRef.get("$ref")); @@ -607,4 +607,123 @@ public void testVisitedSetIsSharedCorrectly() { assertNotNull("Circular reference to rule1 should be detected", rule1CircularRef); assertEquals("Should contain circular reference marker", "circular", rule1CircularRef.get("$ref")); } + + @Test + public void testConditionDeepCopyCopiesNestedConditions() { + Condition inner = new Condition(); + inner.setConditionTypeId("inner"); + Condition outer = new Condition(); + outer.setConditionTypeId("outer"); + outer.getParameterValues().put("c", inner); + + Condition copy = outer.deepCopy(); + assertNotSame(outer, copy); + Condition copyInner = (Condition) copy.getParameterValues().get("c"); + assertNotSame(inner, copyInner); + assertEquals("inner", copyInner.getConditionTypeId()); + } + + @Test(expected = IllegalStateException.class) + public void testConditionDeepCopyRejectsCycle() { + Condition a = new Condition(); + a.setConditionTypeId("a"); + Condition b = new Condition(); + b.setConditionTypeId("b"); + a.getParameterValues().put("child", b); + b.getParameterValues().put("child", a); + a.deepCopy(); + } + + @Test + public void testNewIdentityVisitedSetUsesReferenceIdentity() { + Set visited = YamlUtils.newIdentityVisitedSet(); + String a = new String("x"); + String b = new String("x"); + assertTrue(visited.add(a)); + assertTrue(visited.add(b)); + assertEquals("equal strings are distinct objects for identity set", 2, visited.size()); + } + + @Test + public void testYamlMapBuilderMergeObjectNullIsNoOp() { + Map map = YamlUtils.YamlMapBuilder.create() + .put("k", "v") + .mergeObject(null) + .build(); + assertEquals(1, map.size()); + assertEquals("v", map.get("k")); + } + + @Test + public void testYamlMapBuilderMergeObjectCopiesEntries() { + Map extra = new LinkedHashMap<>(); + extra.put("a", 1); + extra.put("b", 2); + Map map = YamlUtils.YamlMapBuilder.create() + .put("z", 0) + .mergeObject(extra) + .build(); + assertEquals(3, map.size()); + assertEquals(Integer.valueOf(0), map.get("z")); + assertEquals(Integer.valueOf(1), map.get("a")); + assertEquals(Integer.valueOf(2), map.get("b")); + } + + @Test + public void testToYamlValueMaxDepthZeroReturnsPlaceholder() { + assertEquals("", YamlUtils.toYamlValue("anything", null, 0)); + } + + @Test + public void testToYamlValueEmptyMapWithDepth() { + @SuppressWarnings("unchecked") + Map out = (Map) YamlUtils.toYamlValue(Collections.emptyMap(), null, 5); + assertNotNull(out); + assertTrue(out.isEmpty()); + } + + @Test + public void testToYamlValueSortsMapKeysLexicographically() { + Map in = new LinkedHashMap<>(); + in.put("z", 1); + in.put("a", 2); + @SuppressWarnings("unchecked") + Map out = (Map) YamlUtils.toYamlValue(in, null, 10); + assertEquals(Arrays.asList("a", "z"), new ArrayList<>(out.keySet())); + } + + @Test + public void testToYamlValueNonStringMapKeysBecomeStrings() { + Map in = new HashMap<>(); + in.put(10, "ten"); + in.put(2, "two"); + @SuppressWarnings("unchecked") + Map out = (Map) YamlUtils.toYamlValue(in, null, 10); + assertTrue(out.keySet().stream().allMatch(k -> k instanceof String)); + assertEquals("two", out.get("2")); + assertEquals("ten", out.get("10")); + } + + @Test + public void testToYamlValueTwoArgDelegatesToUnboundedDepth() { + Condition c = new Condition(); + c.setConditionTypeId("c"); + c.getParameterValues().put("n", 1); + Object result = YamlUtils.toYamlValue(c, YamlUtils.newIdentityVisitedSet()); + assertTrue(result instanceof Map); + } + + @Test + public void testYamlConvertibleDefaultToYamlWithVisitedOnly() { + YamlUtils.YamlConvertible convertible = new YamlUtils.YamlConvertible() { + @Override + public Map toYaml(Set visited, int maxDepth) { + Map m = new LinkedHashMap<>(); + m.put("depth", maxDepth); + return m; + } + }; + Map map = convertible.toYaml(null); + assertEquals(Integer.valueOf(20), map.get("depth")); + } } diff --git a/pom.xml b/pom.xml index d4825e7ec3..5a2c70fd02 100644 --- a/pom.xml +++ b/pom.xml @@ -80,6 +80,7 @@ 2.3 3.10 2.19.0 + 1.7.36 9.12.2 2.4.0 From 4bd200a7cfdd381b244aa7b3ccb1f7dacc3a476e Mon Sep 17 00:00:00 2001 From: Serge Huber Date: Sat, 16 May 2026 19:06:46 +0200 Subject: [PATCH 12/21] UNOMI-879: Address PR review feedback for unified CRUD shell Fix condition undeploy, rule list performance, null-safety and pagination in CRUD commands, thread-safe consent date formatting, read-only rulestats behavior, segment ID completion, and BOM httpclient dependency management. --- bom/pom.xml | 6 --- tools/shell-dev-commands/pom.xml | 1 + .../dev/commands/UndeployDefinition.java | 2 +- .../commands/consents/ConsentCrudCommand.java | 16 +++++--- .../dev/commands/goals/GoalCrudCommand.java | 2 +- .../commands/personas/PersonaCrudCommand.java | 5 ++- .../dev/commands/rules/RuleCrudCommand.java | 40 ++++++++++++++++++- .../rules/RuleStatisticsCrudCommand.java | 18 ++++----- .../commands/segments/SegmentCrudCommand.java | 4 +- 9 files changed, 66 insertions(+), 28 deletions(-) diff --git a/bom/pom.xml b/bom/pom.xml index c63868276b..17b0f37529 100644 --- a/bom/pom.xml +++ b/bom/pom.xml @@ -270,12 +270,6 @@ httpclient-osgi ${httpclient-osgi.version} - - org.apache.httpcomponents - httpclient-osgi - ${httpclient-osgi.version} - bundle - org.apache.kafka kafka-clients diff --git a/tools/shell-dev-commands/pom.xml b/tools/shell-dev-commands/pom.xml index e62a637589..807677a638 100644 --- a/tools/shell-dev-commands/pom.xml +++ b/tools/shell-dev-commands/pom.xml @@ -92,6 +92,7 @@ org.apache.httpcomponents httpclient-osgi + ${httpclient-osgi.version} bundle provided diff --git a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/UndeployDefinition.java b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/UndeployDefinition.java index 6ed184d2b3..70e8ccd7d8 100644 --- a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/UndeployDefinition.java +++ b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/UndeployDefinition.java @@ -50,7 +50,7 @@ protected boolean processDefinitionByType(String definitionType, URL definitionU switch (definitionType) { case CONDITION_DEFINITION_TYPE: ConditionType conditionType = readDefinition(definitionURL, ConditionType.class); - definitionsService.removeActionType(conditionType.getItemId()); + definitionsService.removeConditionType(conditionType.getItemId()); return true; case ACTION_DEFINITION_TYPE: ActionType actionType = readDefinition(definitionURL, ActionType.class); diff --git a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/consents/ConsentCrudCommand.java b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/consents/ConsentCrudCommand.java index ed59c42882..0590c0c2f3 100644 --- a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/consents/ConsentCrudCommand.java +++ b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/consents/ConsentCrudCommand.java @@ -28,6 +28,7 @@ import org.osgi.service.component.annotations.Component; import org.osgi.service.component.annotations.Reference; +import java.text.DateFormat; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.HashMap; @@ -42,7 +43,9 @@ public class ConsentCrudCommand extends BaseCrudCommand { private static final ObjectMapper OBJECT_MAPPER = new CustomObjectMapper(); - private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ"); + private static final String CONSENT_DATE_FORMAT_PATTERN = "yyyy-MM-dd'T'HH:mm:ss.SSSZ"; + private static final ThreadLocal DATE_FORMAT = ThreadLocal.withInitial( + () -> new SimpleDateFormat(CONSENT_DATE_FORMAT_PATTERN)); private static final List PROPERTY_NAMES = List.of( "profileId", "scope", "typeIdentifier", "status", "statusDate", "revokeDate" ); @@ -66,20 +69,21 @@ protected PartialList getItems(Query query) { PartialList profiles = profileService.search(query, Profile.class); List> consents = new ArrayList<>(); + DateFormat dateFormat = DATE_FORMAT.get(); for (Profile profile : profiles.getList()) { Map profileProperties = profile.getProperties(); if (profileProperties.containsKey("consents")) { @SuppressWarnings("unchecked") Map profileConsents = (Map) profileProperties.get("consents"); for (Map.Entry entry : profileConsents.entrySet()) { - Map consentMap = entry.getValue().toMap(DATE_FORMAT); + Map consentMap = entry.getValue().toMap(dateFormat); consentMap.put("profileId", profile.getItemId()); consents.add(consentMap); } } } - return new PartialList>(consents, profiles.getOffset(), profiles.getPageSize(), profiles.getTotalSize(), PartialList.Relation.EQUAL); + return paginateList(consents, query); } @Override @@ -125,7 +129,7 @@ public Map read(String id) { return null; } - Map consentMap = consent.toMap(DATE_FORMAT); + Map consentMap = consent.toMap(DATE_FORMAT.get()); consentMap.put("profileId", profileId); return consentMap; } @@ -143,7 +147,7 @@ public String create(Map properties) { } try { - Consent consent = new Consent(properties, DATE_FORMAT); + Consent consent = new Consent(properties, DATE_FORMAT.get()); Map profileProperties = profile.getProperties(); @SuppressWarnings("unchecked") @@ -178,7 +182,7 @@ public void update(String id, Map properties) { } try { - Consent consent = new Consent(properties, DATE_FORMAT); + Consent consent = new Consent(properties, DATE_FORMAT.get()); Map profileProperties = profile.getProperties(); @SuppressWarnings("unchecked") diff --git a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/goals/GoalCrudCommand.java b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/goals/GoalCrudCommand.java index 38a7e77efa..cf0fa36829 100644 --- a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/goals/GoalCrudCommand.java +++ b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/goals/GoalCrudCommand.java @@ -65,7 +65,7 @@ protected PartialList getItems(Query query) { .map(metadata -> goalsService.getGoal(metadata.getId())) .filter(goal -> goal != null) .collect(Collectors.toList()); - return new PartialList<>(goals, goals.size(), 0, goals.size(), null); + return new PartialList<>(goals, query.getOffset(), goals.size(), goals.size(), PartialList.Relation.EQUAL); } @Override diff --git a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/personas/PersonaCrudCommand.java b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/personas/PersonaCrudCommand.java index 45e0b88459..5633e89c49 100644 --- a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/personas/PersonaCrudCommand.java +++ b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/personas/PersonaCrudCommand.java @@ -72,13 +72,16 @@ protected PartialList getItems(Query query) { @Override protected String[] buildRow(Object item) { Persona persona = (Persona) item; + Object lastUpdated = persona.getSystemProperties() != null + ? persona.getSystemProperties().get("lastUpdated") + : null; return new String[] { persona.getItemId(), (String) persona.getProperty("firstName"), (String) persona.getProperty("lastName"), (String) persona.getProperty("email"), (String) persona.getProperty("description"), - persona.getSystemProperties().get("lastUpdated").toString() + lastUpdated != null ? lastUpdated.toString() : "" }; } diff --git a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/rules/RuleCrudCommand.java b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/rules/RuleCrudCommand.java index 64d523926f..f99e5ad940 100644 --- a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/rules/RuleCrudCommand.java +++ b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/rules/RuleCrudCommand.java @@ -21,9 +21,11 @@ import org.apache.unomi.api.Metadata; import org.apache.unomi.api.PartialList; import org.apache.unomi.api.query.Query; +import org.apache.karaf.shell.support.table.ShellTable; import org.apache.unomi.api.rules.Rule; import org.apache.unomi.api.rules.RuleStatistics; import org.apache.unomi.api.services.RulesService; +import org.apache.unomi.common.DataTable; import org.apache.unomi.persistence.spi.CustomObjectMapper; import org.apache.unomi.shell.dev.services.BaseCrudCommand; import org.apache.unomi.shell.dev.services.CrudCommand; @@ -31,6 +33,7 @@ import org.osgi.service.component.annotations.Component; import org.osgi.service.component.annotations.Reference; +import java.io.PrintStream; import java.util.ArrayList; import java.util.List; import java.util.Map; @@ -51,6 +54,9 @@ public class RuleCrudCommand extends BaseCrudCommand { @Reference private RulesService rulesService; + /** Cached for the duration of a single list/query operation (see {@link #buildQuery}). */ + private Map statisticsSnapshot; + @Override public String getObjectType() { return "rule"; @@ -78,11 +84,43 @@ protected PartialList getItems(Query query) { return rulesService.getRuleDetails(query); } + @Override + protected DataTable buildDataTable() { + try { + statisticsSnapshot = rulesService.getAllRuleStatistics(); + return super.buildDataTable(); + } finally { + statisticsSnapshot = null; + } + } + + @Override + public void buildRows(ShellTable table, int maxEntries) { + try { + statisticsSnapshot = rulesService.getAllRuleStatistics(); + super.buildRows(table, maxEntries); + } finally { + statisticsSnapshot = null; + } + } + + @Override + public void buildCsvOutput(PrintStream console, String[] headers, int limit) throws Exception { + try { + statisticsSnapshot = rulesService.getAllRuleStatistics(); + super.buildCsvOutput(console, headers, limit); + } finally { + statisticsSnapshot = null; + } + } + @Override protected Comparable[] buildRow(Object item) { Rule rule = (Rule) item; String ruleId = rule.getItemId(); - Map allRuleStatistics = rulesService.getAllRuleStatistics(); + Map allRuleStatistics = statisticsSnapshot != null + ? statisticsSnapshot + : rulesService.getAllRuleStatistics(); ArrayList rowData = new ArrayList<>(); rowData.add(rule.getMetadata().isEnabled() ? "x" : ""); diff --git a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/rules/RuleStatisticsCrudCommand.java b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/rules/RuleStatisticsCrudCommand.java index 8a0507bdd5..84baa0e08d 100644 --- a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/rules/RuleStatisticsCrudCommand.java +++ b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/rules/RuleStatisticsCrudCommand.java @@ -90,23 +90,21 @@ public Map read(String id) { @Override public String create(Map properties) { - // Note: RulesService doesn't provide a direct way to create rule statistics - // They are automatically managed by the rules engine - return null; + throw new UnsupportedOperationException( + "Rule statistics are managed automatically by the rules engine and cannot be created via the shell."); } @Override public void update(String id, Map properties) { - // Note: RulesService doesn't provide a direct way to update rule statistics - // They are automatically managed by the rules engine + throw new UnsupportedOperationException( + "Rule statistics are managed automatically by the rules engine and cannot be updated via the shell."); } @Override public void delete(String id) { - // Note: RulesService doesn't provide a direct way to delete individual rule statistics - // They are automatically managed by the rules engine - // You can use resetAllRuleStatistics() to reset all statistics to zero - rulesService.resetAllRuleStatistics(); + throw new UnsupportedOperationException( + "Rule statistics cannot be deleted individually via the shell (requested id: " + id + + "). Statistics are maintained by the rules engine."); } @Override @@ -129,7 +127,7 @@ public String getPropertiesHelp() { "- localActionsTime: Time spent executing actions on this node since last sync (ms)", "- lastSyncDate: Date of the last synchronization with the cluster", "", - "Note: Use 'unomi:crud rulestats reset' to reset all rule statistics to zero." + "Supported operations: list, read, help only (create, update, and delete are not supported)." ); } } diff --git a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/segments/SegmentCrudCommand.java b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/segments/SegmentCrudCommand.java index 85e16aecc7..7120184575 100644 --- a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/segments/SegmentCrudCommand.java +++ b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/segments/SegmentCrudCommand.java @@ -172,8 +172,8 @@ public List completeId(String prefix) { try { // If prefix is not empty, use it to filter results if (!prefix.isEmpty()) { - Condition condition = new Condition(definitionsService.getConditionType("booleanCondition")); - condition.setParameter("operator", "startsWith"); + Condition condition = new Condition(definitionsService.getConditionType("sessionPropertyCondition")); + condition.setParameter("comparisonOperator", "startsWith"); condition.setParameter("propertyName", "itemId"); condition.setParameter("propertyValue", prefix); query.setCondition(condition); From 83c6a689cb961de1820b3c29630ecf7214d21c05 Mon Sep 17 00:00:00 2001 From: Serge Huber Date: Sun, 17 May 2026 08:05:18 +0200 Subject: [PATCH 13/21] UNOMI-888: Address PR review feedback for router Javadoc Align documentation with actual import/export behavior, fix configType and transport-mode wording (kafka/nobroker vs direct), validate column separator length, and make getCamelContext() a default interface method. --- .../unomi/router/api/IRouterCamelContext.java | 7 +++--- .../router/api/ImportExportConfiguration.java | 22 +++++++++++++------ .../unomi/router/api/ProfileToImport.java | 14 +++++++----- .../BadProfileDataFormatException.java | 18 +++++++-------- .../api/services/ProfileExportService.java | 18 +++++---------- .../api/services/ProfileImportService.java | 4 ++-- .../router/core/bean/CollectProfileBean.java | 3 +-- .../core/context/RouterCamelContext.java | 6 ++--- .../ImportRouteCompletionProcessor.java | 4 ++-- .../ProfileExportCollectRouteBuilder.java | 4 ++-- .../ProfileExportProducerRouteBuilder.java | 4 ++-- .../ProfileImportFromSourceRouteBuilder.java | 4 ++-- .../ProfileImportOneShotRouteBuilder.java | 4 ++-- .../ProfileImportToUnomiRouteBuilder.java | 4 ++-- .../route/RouterAbstractRouteBuilder.java | 9 ++++---- 15 files changed, 64 insertions(+), 61 deletions(-) diff --git a/extensions/router/router-api/src/main/java/org/apache/unomi/router/api/IRouterCamelContext.java b/extensions/router/router-api/src/main/java/org/apache/unomi/router/api/IRouterCamelContext.java index b525ddc474..476b36e82f 100644 --- a/extensions/router/router-api/src/main/java/org/apache/unomi/router/api/IRouterCamelContext.java +++ b/extensions/router/router-api/src/main/java/org/apache/unomi/router/api/IRouterCamelContext.java @@ -38,7 +38,6 @@ * *

    * - * @see org.apache.unomi.router.core.context.RouterCamelContext * @since 1.0 */ public interface IRouterCamelContext { @@ -88,7 +87,9 @@ public interface IRouterCamelContext { * The API uses {@link Object} so consumers of this module are not required to depend on Camel at compile time. * Callers that ship Camel may cast to {@code org.apache.camel.CamelContext}. * - * @return the Camel context instance, or {@code null} if not initialized + * @return the Camel context instance, or {@code null} if not initialized or not exposed */ - Object getCamelContext(); + default Object getCamelContext() { + return null; + } } diff --git a/extensions/router/router-api/src/main/java/org/apache/unomi/router/api/ImportExportConfiguration.java b/extensions/router/router-api/src/main/java/org/apache/unomi/router/api/ImportExportConfiguration.java index 1695b922d0..7c9b5e2382 100644 --- a/extensions/router/router-api/src/main/java/org/apache/unomi/router/api/ImportExportConfiguration.java +++ b/extensions/router/router-api/src/main/java/org/apache/unomi/router/api/ImportExportConfiguration.java @@ -47,8 +47,10 @@ * *

    Configuration properties include: *

      - *
    • name - unique identifier for the configuration
    • - *
    • configType - type of configuration (import/export)
    • + *
    • itemId - persisted identifier used by router services and routes ({@link Item#getItemId()})
    • + *
    • name - human-readable display name (not used as the route or persistence key)
    • + *
    • configType - scheduling mode ({@link RouterConstants#IMPORT_EXPORT_CONFIG_TYPE_RECURRENT} or + * {@link RouterConstants#IMPORT_EXPORT_CONFIG_TYPE_ONESHOT})
    • *
    • columnSeparator - character used to separate columns (default: ",")
    • *
    • lineSeparator - character used to separate lines (default: "\n")
    • *
    • multiValueSeparator - character used to separate multiple values (default: ";")
    • @@ -121,16 +123,18 @@ public void setDescription(String description) { /** - * Retrieves the configuration type (for example import vs export semantics used by the router). + * Returns the scheduling mode for this configuration ({@code recurrent} or {@code oneshot}). * - * @return the config type of this configuration + * @return {@link RouterConstants#IMPORT_EXPORT_CONFIG_TYPE_RECURRENT} or + * {@link RouterConstants#IMPORT_EXPORT_CONFIG_TYPE_ONESHOT} */ public String getConfigType() { return this.configType; } /** - * Sets the configuration type. + * Sets the scheduling mode for this configuration. * - * @param configType the config type for this configuration + * @param configType {@link RouterConstants#IMPORT_EXPORT_CONFIG_TYPE_RECURRENT} or + * {@link RouterConstants#IMPORT_EXPORT_CONFIG_TYPE_ONESHOT} */ public void setConfigType(String configType) { this.configType = configType; @@ -202,10 +206,14 @@ public String getColumnSeparator() { /** * Sets the column separator used when reading or writing delimited text (typically CSV). * - * @param columnSeparator the column delimiter; defaults to {@code ","} when not overridden + * @param columnSeparator the column delimiter; must be exactly one character when non-null + * @throws IllegalArgumentException if {@code columnSeparator} is empty or longer than one character */ public void setColumnSeparator(String columnSeparator) { if (columnSeparator != null) { + if (columnSeparator.length() != 1) { + throw new IllegalArgumentException("columnSeparator must be exactly one character"); + } this.columnSeparator = columnSeparator; } } diff --git a/extensions/router/router-api/src/main/java/org/apache/unomi/router/api/ProfileToImport.java b/extensions/router/router-api/src/main/java/org/apache/unomi/router/api/ProfileToImport.java index bb87cd375f..957ac88e9e 100644 --- a/extensions/router/router-api/src/main/java/org/apache/unomi/router/api/ProfileToImport.java +++ b/extensions/router/router-api/src/main/java/org/apache/unomi/router/api/ProfileToImport.java @@ -31,7 +31,7 @@ *
    • Controls which properties should be overwritten during import
    • *
    • Specifies the property used for merging with existing profiles
    • *
    • Handles profile deletion flags
    • - *
    • Controls overwrite behavior for existing profiles
    • + *
    • Controls merge vs full-replace behavior for existing profiles (see {@link #isOverwriteExistingProfiles()})
    • *
    *

    * @@ -100,19 +100,21 @@ public void setProfileToDelete(boolean profileToDelete) { } /** - * Checks if existing profiles should be overwritten during import. - * When true, all properties of existing profiles will be overwritten with imported data. + * Indicates whether selective property updates are enabled for existing profiles. + * When {@code true} and {@link #getPropertiesToOverwrite()} is non-empty, only the listed properties + * are updated on a matching profile. Otherwise the entire properties map is replaced. * - * @return true if existing profiles should be overwritten, false for selective updates + * @return {@code true} to apply selective overwrites when {@code propertiesToOverwrite} is set */ public boolean isOverwriteExistingProfiles() { return this.overwriteExistingProfiles; } /** - * Sets whether existing profiles should be completely overwritten during import. + * Sets whether selective property updates are enabled for existing profiles. * - * @param overwriteExistingProfiles true to overwrite all properties, false for selective updates + * @param overwriteExistingProfiles {@code true} to update only {@link #getPropertiesToOverwrite()} + * when that list is non-empty; {@code false} replaces the full properties map */ public void setOverwriteExistingProfiles(boolean overwriteExistingProfiles) { this.overwriteExistingProfiles = overwriteExistingProfiles; diff --git a/extensions/router/router-api/src/main/java/org/apache/unomi/router/api/exceptions/BadProfileDataFormatException.java b/extensions/router/router-api/src/main/java/org/apache/unomi/router/api/exceptions/BadProfileDataFormatException.java index 731e377e5c..c4156e929b 100644 --- a/extensions/router/router-api/src/main/java/org/apache/unomi/router/api/exceptions/BadProfileDataFormatException.java +++ b/extensions/router/router-api/src/main/java/org/apache/unomi/router/api/exceptions/BadProfileDataFormatException.java @@ -17,25 +17,23 @@ package org.apache.unomi.router.api.exceptions; /** - * Exception thrown when profile data cannot be properly parsed or formatted during import/export operations. - * This exception indicates issues with the structure or content of profile data that prevent it from being - * properly processed by the Unomi router. + * Exception thrown when profile import line data cannot be parsed or converted during import processing. + * Indicates issues with CSV structure, column mapping, or property value conversion on an import line. * *

    Common scenarios where this exception is thrown: *

      - *
    • Invalid CSV format in import files
    • - *
    • Missing required profile fields
    • - *
    • Incorrect data types for profile properties
    • + *
    • Invalid CSV format or column count mismatch on an import line
    • + *
    • Missing required profile fields in the mapping
    • + *
    • Property value conversion failures (e.g. unsupported type for a mapped field)
    • *
    • Malformed multi-value fields
    • - *
    • Invalid date formats
    • + *
    • Empty lines in import files
    • *
    *

    * *

    Usage in Unomi: *

      - *
    • Thrown by profile import processors
    • - *
    • Used in data validation steps
    • - *
    • Caught by error handling routes
    • + *
    • Thrown by import line processors (e.g. {@code LineSplitProcessor})
    • + *
    • Handled by import route error handlers
    • *
    *

    * diff --git a/extensions/router/router-api/src/main/java/org/apache/unomi/router/api/services/ProfileExportService.java b/extensions/router/router-api/src/main/java/org/apache/unomi/router/api/services/ProfileExportService.java index 37b921c788..ccdc3711b6 100644 --- a/extensions/router/router-api/src/main/java/org/apache/unomi/router/api/services/ProfileExportService.java +++ b/extensions/router/router-api/src/main/java/org/apache/unomi/router/api/services/ProfileExportService.java @@ -62,27 +62,21 @@ public interface ProfileExportService { /** * Extracts profiles belonging to a specified segment and formats them for export. - * This method handles the bulk export operation, including: - * - Querying profiles based on segment criteria - * - Formatting profiles according to export configuration - * - Generating the export content + * Implementations typically query profiles by segment, build CSV content (including line separators + * between rows), append an execution record to the configuration, and persist the updated configuration. * * @param exportConfiguration the configuration specifying export parameters and format - * @return a String containing the formatted export data + * @return CSV (or configured delimited) content for the extracted profiles */ String extractProfilesBySegment(ExportConfiguration exportConfiguration); /** - * Converts a single profile to a CSV line format according to the export configuration. - * This method handles the formatting of individual profiles, including: - * - Property selection and ordering - * - Value formatting - * - Multi-value handling - * - Line separator management + * Converts a single profile to one delimited row according to the export configuration mapping. + * Does not append line separators; callers or export routes add separators between rows. * * @param profile the profile to convert * @param exportConfiguration the configuration specifying the export format - * @return a String containing the CSV-formatted profile data + * @return one row of delimited profile data (no trailing line separator) */ String convertProfileToCSVLine(Profile profile, ExportConfiguration exportConfiguration); diff --git a/extensions/router/router-api/src/main/java/org/apache/unomi/router/api/services/ProfileImportService.java b/extensions/router/router-api/src/main/java/org/apache/unomi/router/api/services/ProfileImportService.java index fc1fe931c8..0d008bbee7 100644 --- a/extensions/router/router-api/src/main/java/org/apache/unomi/router/api/services/ProfileImportService.java +++ b/extensions/router/router-api/src/main/java/org/apache/unomi/router/api/services/ProfileImportService.java @@ -44,10 +44,10 @@ * *

    Implementation considerations: *

      - *
    • Must handle profile merging strategies
    • + *
    • Must handle profile merging strategies defined on {@link ProfileToImport}
    • *
    • Should implement proper error handling
    • *
    • Must maintain data consistency
    • - *
    • Should handle property type conversions
    • + *
    • Expects property values already parsed (type conversion is done upstream, e.g. by import processors)
    • *
    *

    * diff --git a/extensions/router/router-core/src/main/java/org/apache/unomi/router/core/bean/CollectProfileBean.java b/extensions/router/router-core/src/main/java/org/apache/unomi/router/core/bean/CollectProfileBean.java index 43eb8f61da..ae31b63006 100644 --- a/extensions/router/router-core/src/main/java/org/apache/unomi/router/core/bean/CollectProfileBean.java +++ b/extensions/router/router-core/src/main/java/org/apache/unomi/router/core/bean/CollectProfileBean.java @@ -28,9 +28,8 @@ * *

    Features: *

      - *
    • Segment-based profile extraction
    • + *
    • Segment-based profile extraction via persistence queries
    • *
    • Integration with Unomi's persistence service
    • - *
    • Batch profile retrieval capabilities
    • *
    *

    * diff --git a/extensions/router/router-core/src/main/java/org/apache/unomi/router/core/context/RouterCamelContext.java b/extensions/router/router-core/src/main/java/org/apache/unomi/router/core/context/RouterCamelContext.java index ae23e5ebc4..cfd167d04f 100644 --- a/extensions/router/router-core/src/main/java/org/apache/unomi/router/core/context/RouterCamelContext.java +++ b/extensions/router/router-core/src/main/java/org/apache/unomi/router/core/context/RouterCamelContext.java @@ -59,8 +59,8 @@ *
  • Sets up import and export routes
  • *
  • Handles route configuration updates
  • *
  • Manages route lifecycle (start/stop/update)
  • - *
  • Provides monitoring through event notifications
  • - *
  • Supports both Kafka and direct endpoints
  • + *
  • Supports Kafka ({@link RouterConstants#CONFIG_TYPE_KAFKA}) and in-process + * {@code direct:} endpoints when configured as {@link RouterConstants#CONFIG_TYPE_NOBROKER}
  • * *

    * @@ -97,7 +97,7 @@ public class RouterCamelContext implements IRouterCamelContext { private Integer configsRefreshInterval = 1000; private ScheduledFuture scheduledFuture; - /** Event topic fired when a router configuration or route is removed (reserved for integrations). */ + /** Reserved event topic identifier for future remove notifications (not published by the current implementation). */ public static String EVENT_ID_REMOVE = "org.apache.unomi.router.event.remove"; /** Event topic related to import lifecycle (reserved for integrations). */ public static String EVENT_ID_IMPORT = "org.apache.unomi.router.event.import"; diff --git a/extensions/router/router-core/src/main/java/org/apache/unomi/router/core/processor/ImportRouteCompletionProcessor.java b/extensions/router/router-core/src/main/java/org/apache/unomi/router/core/processor/ImportRouteCompletionProcessor.java index 76e82e0d8a..e34d965b62 100644 --- a/extensions/router/router-core/src/main/java/org/apache/unomi/router/core/processor/ImportRouteCompletionProcessor.java +++ b/extensions/router/router-core/src/main/java/org/apache/unomi/router/core/processor/ImportRouteCompletionProcessor.java @@ -32,7 +32,7 @@ * *

    The processor performs the following operations: *

      - *
    • Collects import statistics (success, failure, ignore counts)
    • + *
    • Collects import statistics persisted on the configuration (success and failure counts, plus error details)
    • *
    • Manages error reporting with configurable limits
    • *
    • Updates import configuration status
    • *
    • Maintains execution history
    • @@ -61,7 +61,7 @@ public class ImportRouteCompletionProcessor implements Processor { *

      This method: *

        *
      • Identifies the import configuration (one-shot or recurring)
      • - *
      • Counts successful, failed, and ignored imports
      • + *
      • Counts successful and failed imports (unrecognized line types are skipped and not persisted)
      • *
      • Collects error information up to the configured limit
      • *
      • Updates the import configuration with execution results
      • *
      • Sets the final status based on success/failure counts
      • diff --git a/extensions/router/router-core/src/main/java/org/apache/unomi/router/core/route/ProfileExportCollectRouteBuilder.java b/extensions/router/router-core/src/main/java/org/apache/unomi/router/core/route/ProfileExportCollectRouteBuilder.java index 3c690a36e5..9a7a351b86 100644 --- a/extensions/router/router-core/src/main/java/org/apache/unomi/router/core/route/ProfileExportCollectRouteBuilder.java +++ b/extensions/router/router-core/src/main/java/org/apache/unomi/router/core/route/ProfileExportCollectRouteBuilder.java @@ -42,7 +42,7 @@ *
      • Support for multiple export configurations
      • *
      • Configurable collection intervals
      • *
      • Security through endpoint allowlist
      • - *
      • Support for both Kafka and direct endpoints
      • + *
      • Support for Kafka and in-process {@code direct:} endpoints ({@link RouterConstants#CONFIG_TYPE_KAFKA} / {@link RouterConstants#CONFIG_TYPE_NOBROKER})
      • *
      *

      * @@ -62,7 +62,7 @@ public class ProfileExportCollectRouteBuilder extends RouterAbstractRouteBuilder * Constructs a new route builder with Kafka configuration. * * @param kafkaProps map containing Kafka configuration properties - * @param configType the type of configuration (kafka/direct) + * @param configType {@link RouterConstants#CONFIG_TYPE_KAFKA} or {@link RouterConstants#CONFIG_TYPE_NOBROKER} */ public ProfileExportCollectRouteBuilder(Map kafkaProps, String configType) { super(kafkaProps, configType); diff --git a/extensions/router/router-core/src/main/java/org/apache/unomi/router/core/route/ProfileExportProducerRouteBuilder.java b/extensions/router/router-core/src/main/java/org/apache/unomi/router/core/route/ProfileExportProducerRouteBuilder.java index cc903e84a1..89619b7da9 100644 --- a/extensions/router/router-core/src/main/java/org/apache/unomi/router/core/route/ProfileExportProducerRouteBuilder.java +++ b/extensions/router/router-core/src/main/java/org/apache/unomi/router/core/route/ProfileExportProducerRouteBuilder.java @@ -39,7 +39,7 @@ *
    • Line-by-line processing with aggregation
    • *
    • Support for multiple export destinations
    • *
    • Completion handling and status updates
    • - *
    • Support for both Kafka and direct endpoints
    • + *
    • Support for Kafka and in-process {@code direct:} endpoints ({@link RouterConstants#CONFIG_TYPE_KAFKA} / {@link RouterConstants#CONFIG_TYPE_NOBROKER})
    • *
    *

    * @@ -59,7 +59,7 @@ public class ProfileExportProducerRouteBuilder extends RouterAbstractRouteBuilde * Constructs a new route builder with Kafka configuration. * * @param kafkaProps map containing Kafka configuration properties - * @param configType the type of configuration (kafka/direct) + * @param configType {@link RouterConstants#CONFIG_TYPE_KAFKA} or {@link RouterConstants#CONFIG_TYPE_NOBROKER} */ public ProfileExportProducerRouteBuilder(Map kafkaProps, String configType) { super(kafkaProps, configType); diff --git a/extensions/router/router-core/src/main/java/org/apache/unomi/router/core/route/ProfileImportFromSourceRouteBuilder.java b/extensions/router/router-core/src/main/java/org/apache/unomi/router/core/route/ProfileImportFromSourceRouteBuilder.java index faff0cfb5c..2b24fdbf83 100644 --- a/extensions/router/router-core/src/main/java/org/apache/unomi/router/core/route/ProfileImportFromSourceRouteBuilder.java +++ b/extensions/router/router-core/src/main/java/org/apache/unomi/router/core/route/ProfileImportFromSourceRouteBuilder.java @@ -47,7 +47,7 @@ *
  • Line-by-line processing of import data
  • *
  • Error handling and failure reporting
  • *
  • Configuration validation and status updates
  • - *
  • Support for both Kafka and direct endpoints
  • + *
  • Support for Kafka and in-process {@code direct:} endpoints ({@link RouterConstants#CONFIG_TYPE_KAFKA} / {@link RouterConstants#CONFIG_TYPE_NOBROKER})
  • *
  • Graceful shutdown handling
  • * *

    @@ -68,7 +68,7 @@ public class ProfileImportFromSourceRouteBuilder extends RouterAbstractRouteBuil * Constructs a new route builder with Kafka configuration. * * @param kafkaProps map containing Kafka configuration properties - * @param configType the type of configuration (kafka/direct) + * @param configType {@link RouterConstants#CONFIG_TYPE_KAFKA} or {@link RouterConstants#CONFIG_TYPE_NOBROKER} */ public ProfileImportFromSourceRouteBuilder(Map kafkaProps, String configType) { super(kafkaProps, configType); diff --git a/extensions/router/router-core/src/main/java/org/apache/unomi/router/core/route/ProfileImportOneShotRouteBuilder.java b/extensions/router/router-core/src/main/java/org/apache/unomi/router/core/route/ProfileImportOneShotRouteBuilder.java index 6ac223f3d2..fd9d6c001e 100644 --- a/extensions/router/router-core/src/main/java/org/apache/unomi/router/core/route/ProfileImportOneShotRouteBuilder.java +++ b/extensions/router/router-core/src/main/java/org/apache/unomi/router/core/route/ProfileImportOneShotRouteBuilder.java @@ -39,7 +39,7 @@ *
  • File-based import processing
  • *
  • Configuration lookup from filename
  • *
  • CSV file processing with error handling
  • - *
  • Support for both Kafka and direct endpoints
  • + *
  • Support for Kafka and in-process {@code direct:} endpoints ({@link RouterConstants#CONFIG_TYPE_KAFKA} / {@link RouterConstants#CONFIG_TYPE_NOBROKER})
  • *
  • Automatic file movement after processing
  • *
  • Error reporting and failed file handling
  • * @@ -61,7 +61,7 @@ public class ProfileImportOneShotRouteBuilder extends RouterAbstractRouteBuilder * Constructs a new route builder with Kafka configuration. * * @param kafkaProps map containing Kafka configuration properties - * @param configType the type of configuration (kafka/direct) + * @param configType {@link RouterConstants#CONFIG_TYPE_KAFKA} or {@link RouterConstants#CONFIG_TYPE_NOBROKER} */ public ProfileImportOneShotRouteBuilder(Map kafkaProps, String configType) { super(kafkaProps, configType); diff --git a/extensions/router/router-core/src/main/java/org/apache/unomi/router/core/route/ProfileImportToUnomiRouteBuilder.java b/extensions/router/router-core/src/main/java/org/apache/unomi/router/core/route/ProfileImportToUnomiRouteBuilder.java index 44bebc1491..7f31e57bbb 100644 --- a/extensions/router/router-core/src/main/java/org/apache/unomi/router/core/route/ProfileImportToUnomiRouteBuilder.java +++ b/extensions/router/router-core/src/main/java/org/apache/unomi/router/core/route/ProfileImportToUnomiRouteBuilder.java @@ -36,7 +36,7 @@ *
      *
    • Final processing of imported profiles
    • *
    • Integration with Unomi's storage system
    • - *
    • Support for both Kafka and direct endpoints
    • + *
    • Support for Kafka and in-process {@code direct:} endpoints ({@link RouterConstants#CONFIG_TYPE_KAFKA} / {@link RouterConstants#CONFIG_TYPE_NOBROKER})
    • *
    • Import completion handling
    • *
    • Error handling and reporting
    • *
    @@ -58,7 +58,7 @@ public class ProfileImportToUnomiRouteBuilder extends RouterAbstractRouteBuilder * Constructs a new route builder with Kafka configuration. * * @param kafkaProps map containing Kafka configuration properties - * @param configType the type of configuration (kafka/direct) + * @param configType {@link RouterConstants#CONFIG_TYPE_KAFKA} or {@link RouterConstants#CONFIG_TYPE_NOBROKER} */ public ProfileImportToUnomiRouteBuilder(Map kafkaProps, String configType) { super(kafkaProps, configType); diff --git a/extensions/router/router-core/src/main/java/org/apache/unomi/router/core/route/RouterAbstractRouteBuilder.java b/extensions/router/router-core/src/main/java/org/apache/unomi/router/core/route/RouterAbstractRouteBuilder.java index 5ca80bfffc..69586990fa 100644 --- a/extensions/router/router-core/src/main/java/org/apache/unomi/router/core/route/RouterAbstractRouteBuilder.java +++ b/extensions/router/router-core/src/main/java/org/apache/unomi/router/core/route/RouterAbstractRouteBuilder.java @@ -30,12 +30,13 @@ /** * Abstract base class for all Unomi router route builders. * This class provides common functionality and configuration for both import - * and export routes, supporting both Kafka and direct endpoint configurations. + * and export routes, supporting Kafka ({@link RouterConstants#CONFIG_TYPE_KAFKA}) and in-process + * {@code direct:} buffer endpoints when configured as {@link RouterConstants#CONFIG_TYPE_NOBROKER}. * *

    Features: *

      *
    • Common Kafka configuration handling
    • - *
    • Endpoint URI generation for both Kafka and direct modes
    • + *
    • Endpoint URI generation for Kafka topics or in-vm {@code direct:} buffers
    • *
    • Shared configuration for JSON data format
    • *
    • Profile service integration
    • *
    • Endpoint security through allowlist
    • @@ -73,7 +74,7 @@ public abstract class RouterAbstractRouteBuilder extends RouteBuilder { /** Auto-commit configuration for Kafka */ protected String kafkaAutoCommit; - /** Configuration type (kafka/direct) */ + /** Router transport mode ({@link RouterConstants#CONFIG_TYPE_KAFKA} or {@link RouterConstants#CONFIG_TYPE_NOBROKER}) */ protected String configType; /** List of allowed endpoint schemes */ @@ -86,7 +87,7 @@ public abstract class RouterAbstractRouteBuilder extends RouteBuilder { * Constructs a new route builder with Kafka configuration. * * @param kafkaProps map containing Kafka configuration properties - * @param configType the type of configuration (kafka/direct) + * @param configType {@link RouterConstants#CONFIG_TYPE_KAFKA} or {@link RouterConstants#CONFIG_TYPE_NOBROKER} */ public RouterAbstractRouteBuilder(Map kafkaProps, String configType) { this.kafkaHost = kafkaProps.get("kafkaHost"); From df1dc2a8f1f568ddead960bb679d56af2fa8b2ac Mon Sep 17 00:00:00 2001 From: Serge Huber Date: Mon, 18 May 2026 15:04:49 +0200 Subject: [PATCH 14/21] UNOMI-880 (split A0): migrate Elasticsearch integration tests to Docker Replace the com.github.alexcojocaru:elasticsearch-maven-plugin (binary download + forked JVM) with io.fabric8:docker-maven-plugin so the elasticsearch profile of itests runs ES in a Docker container, mirroring how the opensearch profile already runs OpenSearch. itests/pom.xml (elasticsearch profile) * Add an 9400 property and pass it through the failsafe systemPropertyVariables so tests resolve the HTTP port from a single source. * Replace the elasticsearch-maven-plugin block with a docker-maven-plugin block that starts/stops a docker.elastic.co/elasticsearch/elasticsearch container, binds target/snapshots_repository to /tmp/snapshots_repository, and waits on the HTTP port before the integration-test phase. * Add a chmod -R ugo+rwx on snapshots_repository in the antrun unzip step: the ES container runs as UID 1000, so on Linux CI the bind-mounted snapshot repo otherwise hits access_denied during repository verify. pom.xml (root) * Declare 0.48.0 and add the pluginManagement entry so the elasticsearch profile (and any future user of the plugin) inherits a single version. This is phase A0 of the PR #757 stack split (see docs/PR-757-stack-extraction-tracker.md). It is intentionally small and self-contained: no test/code changes, only the test infrastructure switch. --- itests/pom.xml | 91 ++++++++++++++++++++++++++++++++++---------------- pom.xml | 6 ++++ 2 files changed, 68 insertions(+), 29 deletions(-) diff --git a/itests/pom.xml b/itests/pom.xml index 98e98e9e56..0d0ccde8c5 100644 --- a/itests/pom.xml +++ b/itests/pom.xml @@ -196,6 +196,14 @@ + + + + + + @@ -210,6 +218,9 @@ elasticsearch + + 9400 + true @@ -229,6 +240,7 @@ foo elasticsearch + ${elasticsearch.port} @@ -247,43 +259,64 @@ - com.github.alexcojocaru - elasticsearch-maven-plugin - - 6.29 + io.fabric8 + docker-maven-plugin - - contextElasticSearchITests - 9500 - 9400 - ${elasticsearch.test.version} - true - 120 - - -Xms4g -Xmx4g - - - - false - ${project.build.directory}/snapshots_repository - false - OPTIONS,HEAD,GET,POST,PUT,DELETE - Authorization,X-Requested-With,X-Auth-Token,Content-Type,Content-Length - - + itests-elasticsearch + + + docker.elastic.co/elasticsearch/elasticsearch:${elasticsearch.test.version} + elasticsearch + + + ${elasticsearch.port}:9200 + + + single-node + -Xms8g -Xmx8g -Dcluster.default.index.settings.number_of_replicas=0 + false + false + /tmp/snapshots_repository + false + + + + ${project.build.directory}/snapshots_repository:/tmp/snapshots_repository + + + + + http://localhost:${elasticsearch.port} + GET + 200 + + + + ${project.build.directory}/elasticsearch-port.properties + + + - + + + remove-existing-container + pre-integration-test + + stop + remove + + + start-elasticsearch pre-integration-test - runforked + start + + true + stop-elasticsearch diff --git a/pom.xml b/pom.xml index 25360797d5..f5c1c2aa58 100644 --- a/pom.xml +++ b/pom.xml @@ -146,6 +146,7 @@ 3.21.0 0.16.1 1.0-m5.1 + 0.48.0 v16.20.2 v1.22.19 @@ -865,6 +866,11 @@ dependency-check-maven ${dependency-check.plugin.version} + + io.fabric8 + docker-maven-plugin + ${docker-maven-plugin.version} + From a2e008cc9896abb252717994e554a1408706a8e8 Mon Sep 17 00:00:00 2001 From: Serge Huber Date: Mon, 18 May 2026 15:04:49 +0200 Subject: [PATCH 15/21] UNOMI-921: Replace elasticsearch-maven-plugin with Docker-based Elasticsearch in integration tests Implements the core configuration switch from UNOMI-921: https://issues.apache.org/jira/browse/UNOMI-921 Replace the com.github.alexcojocaru:elasticsearch-maven-plugin (binary download + forked JVM) with io.fabric8:docker-maven-plugin in the elasticsearch profile of itests, mirroring how the opensearch profile already runs OpenSearch in a Docker container. itests/pom.xml (elasticsearch profile) * Add an 9400 property and pass it through the failsafe systemPropertyVariables so tests resolve the HTTP port from a single source (unchanged from the previous 9400). * Replace the elasticsearch-maven-plugin block with a docker-maven-plugin block that runs docker.elastic.co/elasticsearch/elasticsearch:${elasticsearch.test.version}, binds target/snapshots_repository to /tmp/snapshots_repository, and waits on the HTTP port before the integration-test phase. Heap aligned to 8GB (-Xms8g -Xmx8g) to match the OpenSearch configuration and the ES 9 recommendation. Discovery=single-node, replicas=0, xpack.ml and xpack.security disabled. * Container lifecycle matches OpenSearch exactly: pre-integration-test runs stop+remove then start (with showLogs); post-integration-test runs stop only -- container is kept around for inspection. * Add a chmod -R ugo+rwx on snapshots_repository in the antrun unzip step: the ES container runs as UID 1000, so on Linux CI the bind-mounted snapshot repo otherwise hits access_denied during repository verify. pom.xml (root) * Declare 0.48.0 and add the pluginManagement entry so the elasticsearch profile (and any future user of the plugin) inherits a single version. Scope kept minimal for the PR #757 stack split: only the test infrastructure switch lives here. The follow-up UNOMI-921 acceptance items below ship in the platform PR (P) once it lands: * Remove BaseIT.fixDefaultTemplateIfNeeded() and the call in checkSearchEngine() (no longer needed with Docker). * Migrate16xToCurrentVersionIT: replace hardcoded ES_BASE_URL = "http://localhost:9400" with dynamic getSearchPort(). * Drop the comments referring to the elasticsearch-maven-plugin template-override workaround. See docs/PR-757-stack-extraction-tracker.md for the full split plan and how this PR fits in the stack. --- itests/pom.xml | 91 ++++++++++++++++++++++++++++++++++---------------- pom.xml | 6 ++++ 2 files changed, 68 insertions(+), 29 deletions(-) diff --git a/itests/pom.xml b/itests/pom.xml index 98e98e9e56..0d0ccde8c5 100644 --- a/itests/pom.xml +++ b/itests/pom.xml @@ -196,6 +196,14 @@ + + + + + + @@ -210,6 +218,9 @@ elasticsearch + + 9400 + true @@ -229,6 +240,7 @@ foo elasticsearch + ${elasticsearch.port} @@ -247,43 +259,64 @@ - com.github.alexcojocaru - elasticsearch-maven-plugin - - 6.29 + io.fabric8 + docker-maven-plugin - - contextElasticSearchITests - 9500 - 9400 - ${elasticsearch.test.version} - true - 120 - - -Xms4g -Xmx4g - - - - false - ${project.build.directory}/snapshots_repository - false - OPTIONS,HEAD,GET,POST,PUT,DELETE - Authorization,X-Requested-With,X-Auth-Token,Content-Type,Content-Length - - + itests-elasticsearch + + + docker.elastic.co/elasticsearch/elasticsearch:${elasticsearch.test.version} + elasticsearch + + + ${elasticsearch.port}:9200 + + + single-node + -Xms8g -Xmx8g -Dcluster.default.index.settings.number_of_replicas=0 + false + false + /tmp/snapshots_repository + false + + + + ${project.build.directory}/snapshots_repository:/tmp/snapshots_repository + + + + + http://localhost:${elasticsearch.port} + GET + 200 + + + + ${project.build.directory}/elasticsearch-port.properties + + + - + + + remove-existing-container + pre-integration-test + + stop + remove + + + start-elasticsearch pre-integration-test - runforked + start + + true + stop-elasticsearch diff --git a/pom.xml b/pom.xml index 25360797d5..f5c1c2aa58 100644 --- a/pom.xml +++ b/pom.xml @@ -146,6 +146,7 @@ 3.21.0 0.16.1 1.0-m5.1 + 0.48.0 v16.20.2 v1.22.19 @@ -865,6 +866,11 @@ dependency-check-maven ${dependency-check.plugin.version} + + io.fabric8 + docker-maven-plugin + ${docker-maven-plugin.version} + From 944c39bdf91238e937eac739c532eb7083e39db9 Mon Sep 17 00:00:00 2001 From: Serge Huber Date: Sat, 23 May 2026 10:31:07 +0200 Subject: [PATCH 16/21] UNOMI-937: Add build.sh CI mode for non-interactive GitHub Actions Non-interactive prompts when CI, GITHUB_ACTIONS, or BUILD_NON_INTERACTIVE is set. Add --ci (no Karaf, no Maven build cache) and MAVEN_EXTRA_OPTS for matrix ports. --- build.sh | 48 +++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 39 insertions(+), 9 deletions(-) diff --git a/build.sh b/build.sh index 19dd5d0f66..80435acc84 100755 --- a/build.sh +++ b/build.sh @@ -222,13 +222,23 @@ print_progress() { fi } +# Non-interactive when run from CI or when explicitly requested (e.g. GitHub Actions). +is_non_interactive() { + [ -n "${CI:-}" ] || [ -n "${GITHUB_ACTIONS:-}" ] || [ "${BUILD_NON_INTERACTIVE:-}" = "true" ] +} + # Function to prompt for continuation prompt_continue() { local prompt_text="$1" if [ -z "$prompt_text" ]; then prompt_text="Continue?" fi - + + if is_non_interactive; then + print_status "info" "Non-interactive mode: continuing ($prompt_text)" + return 0 + fi + read -p "$prompt_text (y/N) " -n 1 -r echo if [[ ! $REPLY =~ ^[Yy]$ ]]; then @@ -296,6 +306,7 @@ EOF echo -e " ${CYAN}--it-debug-port PORT${NC} Set integration test debug port" echo -e " ${CYAN}--it-debug-suspend${NC} Suspend integration test until debugger connects" echo -e " ${CYAN}--skip-migration-tests${NC} Skip migration-related tests" + echo -e " ${CYAN}--ci${NC} CI mode: no Karaf, no Maven build cache, non-interactive" else cat << "EOF" _ _ _____ _ ____ @@ -329,6 +340,7 @@ EOF echo " --it-debug-port PORT Set integration test debug port" echo " --it-debug-suspend Suspend integration test until debugger connects" echo " --skip-migration-tests Skip migration-related tests" + echo " --ci CI mode: no Karaf, no Maven build cache, non-interactive" fi echo @@ -459,6 +471,11 @@ while [ "$1" != "" ]; do --skip-migration-tests) SKIP_MIGRATION_TESTS=true ;; + --ci) + NO_KARAF=true + USE_MAVEN_CACHE=false + BUILD_NON_INTERACTIVE=true + ;; *) echo "Unknown option: $1" usage @@ -784,10 +801,14 @@ if [ "$MAVEN_OFFLINE" = true ]; then # Warn if purge cache is enabled with offline mode if [ "$PURGE_MAVEN_CACHE" = true ]; then echo "Warning: Purging Maven cache while in offline mode may cause build failures" - read -p "Continue anyway? (y/N) " -n 1 -r - echo - if [[ ! $REPLY =~ ^[Yy]$ ]]; then - exit 1 + if is_non_interactive; then + print_status "warning" "Non-interactive mode: continuing despite purge + offline" + else + read -p "Continue anyway? (y/N) " -n 1 -r + echo + if [[ ! $REPLY =~ ^[Yy]$ ]]; then + exit 1 + fi fi fi fi @@ -797,13 +818,22 @@ if [ "$USE_MAVEN_CACHE" = false ]; then MVN_OPTS="$MVN_OPTS -Dmaven.build.cache.enabled=false" fi +# Extra Maven options (e.g. CI matrix ports: -Delasticsearch.port=9400) +if [ -n "${MAVEN_EXTRA_OPTS:-}" ]; then + MVN_OPTS="$MVN_OPTS $MAVEN_EXTRA_OPTS" +fi + # Verify Maven settings if [ ! -f ~/.m2/settings.xml ]; then echo "Warning: Maven settings.xml not found at ~/.m2/settings.xml" - read -p "Continue anyway? (y/N) " -n 1 -r - echo - if [[ ! $REPLY =~ ^[Yy]$ ]]; then - exit 1 + if is_non_interactive; then + print_status "info" "Non-interactive mode: continuing without ~/.m2/settings.xml" + else + read -p "Continue anyway? (y/N) " -n 1 -r + echo + if [[ ! $REPLY =~ ^[Yy]$ ]]; then + exit 1 + fi fi fi From eb294da69a3e5b28cd31cc75c2f6a260350b4ba0 Mon Sep 17 00:00:00 2001 From: Serge Huber Date: Sat, 23 May 2026 10:32:18 +0200 Subject: [PATCH 17/21] UNOMI-937: Harden flaky integration tests with keepTrying polling Replace fixed sleeps in GraphQLListIT and poll after patch refresh in PatchIT. --- .../java/org/apache/unomi/itests/PatchIT.java | 24 ++++++++++++----- .../unomi/itests/graphql/GraphQLListIT.java | 26 ++++++++++++------- 2 files changed, 35 insertions(+), 15 deletions(-) diff --git a/itests/src/test/java/org/apache/unomi/itests/PatchIT.java b/itests/src/test/java/org/apache/unomi/itests/PatchIT.java index 098a03ad29..1c9cd5a438 100644 --- a/itests/src/test/java/org/apache/unomi/itests/PatchIT.java +++ b/itests/src/test/java/org/apache/unomi/itests/PatchIT.java @@ -49,7 +49,10 @@ public void testPatch() throws IOException { profileService.refresh(); - newCompany = profileService.getPropertyType("company"); + newCompany = keepTrying("Failed waiting for patched property type", + () -> profileService.getPropertyType("company"), + pt -> pt != null && "foo".equals(pt.getDefaultValue()), + DEFAULT_TRYING_TIMEOUT, DEFAULT_TRYING_TRIES); Assert.assertEquals("foo", newCompany.getDefaultValue()); } finally { profileService.setPropertyType(company); @@ -68,7 +71,10 @@ public void testOverride() throws IOException { profileService.refresh(); - newGender = profileService.getPropertyType("gender"); + newGender = keepTrying("Failed waiting for patched property type", + () -> profileService.getPropertyType("gender"), + pt -> pt != null && "foo".equals(pt.getDefaultValue()), + DEFAULT_TRYING_TIMEOUT, DEFAULT_TRYING_TRIES); Assert.assertEquals("foo", newGender.getDefaultValue()); } finally { profileService.setPropertyType(gender); @@ -86,8 +92,8 @@ public void testRemove() throws IOException, InterruptedException { profileService.refresh(); - PropertyType newIncome = profileService.getPropertyType("income"); - Assert.assertNull(newIncome); + waitForNullValue("Failed waiting for property type removal", + () -> profileService.getPropertyType("income"), DEFAULT_TRYING_TIMEOUT, DEFAULT_TRYING_TRIES); } finally { profileService.setPropertyType(income); } @@ -105,7 +111,10 @@ public void testPatchOnConditionType() throws IOException, InterruptedException definitionsService.refresh(); - ConditionType newFormCondition = definitionsService.getConditionType("formEventCondition"); + ConditionType newFormCondition = keepTrying("Failed waiting for patched condition type", + () -> definitionsService.getConditionType("formEventCondition"), + ct -> ct != null && !ct.getMetadata().getSystemTags().contains("profileTags"), + DEFAULT_TRYING_TIMEOUT, DEFAULT_TRYING_TRIES); Assert.assertFalse(newFormCondition.getMetadata().getSystemTags().contains("profileTags")); } finally { definitionsService.setConditionType(formCondition); @@ -124,7 +133,10 @@ public void testPatchOnActionType() throws IOException, InterruptedException { definitionsService.refresh(); - ActionType newMailAction = definitionsService.getActionType("sendMailAction"); + ActionType newMailAction = keepTrying("Failed waiting for patched action type", + () -> definitionsService.getActionType("sendMailAction"), + at -> at != null && !at.getMetadata().getSystemTags().contains("availableToEndUser"), + DEFAULT_TRYING_TIMEOUT, DEFAULT_TRYING_TRIES); Assert.assertFalse(newMailAction.getMetadata().getSystemTags().contains("availableToEndUser")); } finally { definitionsService.setActionType(mailAction); diff --git a/itests/src/test/java/org/apache/unomi/itests/graphql/GraphQLListIT.java b/itests/src/test/java/org/apache/unomi/itests/graphql/GraphQLListIT.java index 96a6eb9865..757904b72c 100644 --- a/itests/src/test/java/org/apache/unomi/itests/graphql/GraphQLListIT.java +++ b/itests/src/test/java/org/apache/unomi/itests/graphql/GraphQLListIT.java @@ -77,15 +77,23 @@ public void testCRUD() throws Exception { refreshPersistence(UserList.class); - Thread.sleep(6000); - - try (CloseableHttpResponse response = post("graphql/list/find-lists.json")) { - final ResponseContext context = ResponseContext.parse(response.getEntity()); - - Assert.assertEquals(1, ((Integer) context.getValue("data.cdp.findLists.totalCount")).intValue()); - Assert.assertEquals("testListId", context.getValue("data.cdp.findLists.edges[0].node.id")); - Assert.assertEquals(profile.getItemId(), context.getValue("data.cdp.findLists.edges[0].node.active.edges[0].node.cdp_profileIDs[0].id")); - } + final ResponseContext findListsContext = keepTrying("Failed waiting for profile in list query", + () -> { + try (CloseableHttpResponse response = post("graphql/list/find-lists.json")) { + return ResponseContext.parse(response.getEntity()); + } + }, + context -> { + Integer totalCount = (Integer) context.getValue("data.cdp.findLists.totalCount"); + if (totalCount == null || totalCount != 1) { + return false; + } + Object profileId = context.getValue("data.cdp.findLists.edges[0].node.active.edges[0].node.cdp_profileIDs[0].id"); + return profile.getItemId().equals(profileId); + }, + DEFAULT_TRYING_TIMEOUT, DEFAULT_TRYING_TRIES); + + Assert.assertEquals("testListId", findListsContext.getValue("data.cdp.findLists.edges[0].node.id")); try (CloseableHttpResponse response = post("graphql/list/delete-list.json")) { final ResponseContext context = ResponseContext.parse(response.getEntity()); From c2f30010c6a1701f331eac783bde4ee1dc56df41 Mon Sep 17 00:00:00 2001 From: Serge Huber Date: Sat, 23 May 2026 10:32:18 +0200 Subject: [PATCH 18/21] UNOMI-937: Add waitForProfileProperty helper for async rule updates Poll profile properties after events in PropertiesUpdateActionIT. --- .../test/java/org/apache/unomi/itests/BaseIT.java | 8 ++++++++ .../unomi/itests/PropertiesUpdateActionIT.java | 14 +++++--------- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/itests/src/test/java/org/apache/unomi/itests/BaseIT.java b/itests/src/test/java/org/apache/unomi/itests/BaseIT.java index 1c1bd6f8c0..676639b0de 100644 --- a/itests/src/test/java/org/apache/unomi/itests/BaseIT.java +++ b/itests/src/test/java/org/apache/unomi/itests/BaseIT.java @@ -467,6 +467,14 @@ protected T keepTrying(String failMessage, Supplier call, Predicate pr return value; } + protected void waitForProfileProperty(String profileId, String propertyName, Object expected) + throws InterruptedException { + keepTrying("Profile " + profileId + " property " + propertyName + " not updated", + () -> profileService.load(profileId), + profile -> profile != null && java.util.Objects.equals(expected, profile.getProperty(propertyName)), + DEFAULT_TRYING_TIMEOUT, DEFAULT_TRYING_TRIES); + } + protected void waitForNullValue(String failMessage, Supplier call, int timeout, int retries) throws InterruptedException { int count = 0; while (call.get() != null) { diff --git a/itests/src/test/java/org/apache/unomi/itests/PropertiesUpdateActionIT.java b/itests/src/test/java/org/apache/unomi/itests/PropertiesUpdateActionIT.java index a1e340680f..e07d2bd5c8 100644 --- a/itests/src/test/java/org/apache/unomi/itests/PropertiesUpdateActionIT.java +++ b/itests/src/test/java/org/apache/unomi/itests/PropertiesUpdateActionIT.java @@ -78,7 +78,7 @@ public void setUp() throws InterruptedException { } @Test - public void testUpdateProperties_CurrentProfile() { + public void testUpdateProperties_CurrentProfile() throws InterruptedException { Profile profile = profileService.load(PROFILE_TARGET_TEST_ID); Assert.assertNull(profile.getProperty("firstName")); @@ -92,16 +92,13 @@ public void testUpdateProperties_CurrentProfile() { updateProperties.setProperty(UpdatePropertiesAction.TARGET_ID_KEY, PROFILE_TARGET_TEST_ID); updateProperties.setProperty(UpdatePropertiesAction.TARGET_TYPE_KEY, "profile"); - int changes = eventService.send(updateProperties); - - LOGGER.info("Changes of the event : {}", changes); + eventService.send(updateProperties); - Assert.assertTrue(changes > 0); - Assert.assertEquals("UPDATED FIRST NAME CURRENT PROFILE", profile.getProperty("firstName")); + waitForProfileProperty(PROFILE_TARGET_TEST_ID, "firstName", "UPDATED FIRST NAME CURRENT PROFILE"); } @Test - public void testUpdateProperties_NotCurrentProfile() { + public void testUpdateProperties_NotCurrentProfile() throws InterruptedException { Profile profile = profileService.load(PROFILE_TARGET_TEST_ID); Profile profileToUpdate = profileService.load(PROFILE_TEST_ID); Assert.assertNull(profileToUpdate.getProperty("firstName")); @@ -117,8 +114,7 @@ public void testUpdateProperties_NotCurrentProfile() { updateProperties.setProperty(UpdatePropertiesAction.TARGET_TYPE_KEY, "profile"); eventService.send(updateProperties); - profileToUpdate = profileService.load(PROFILE_TEST_ID); - Assert.assertEquals("UPDATED FIRST NAME", profileToUpdate.getProperty("firstName")); + waitForProfileProperty(PROFILE_TEST_ID, "firstName", "UPDATED FIRST NAME"); } @Test From 2611dea64f026ea2897bf5c9ef7235d3407f4e28 Mon Sep 17 00:00:00 2001 From: Serge Huber Date: Sat, 23 May 2026 10:32:18 +0200 Subject: [PATCH 19/21] UNOMI-937: Run GitHub Actions unit and IT jobs via build.sh Align CI with local developer workflow; pass matrix ports via MAVEN_EXTRA_OPTS. --- .github/workflows/unomi-ci-build-tests.yml | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/.github/workflows/unomi-ci-build-tests.yml b/.github/workflows/unomi-ci-build-tests.yml index e629253cd6..2533a8b23f 100644 --- a/.github/workflows/unomi-ci-build-tests.yml +++ b/.github/workflows/unomi-ci-build-tests.yml @@ -31,7 +31,7 @@ jobs: sudo apt-get install -y graphviz dot -V - name: Build and Unit tests - run: mvn -U -ntp -e clean install + run: ./build.sh --ci integration-tests: name: Execute integration tests @@ -54,16 +54,22 @@ jobs: distribution: 'temurin' java-version: '17' cache: 'maven' + - name: Install GraphViz + run: | + sudo apt-get update + sudo apt-get install -y graphviz + dot -V - name: Integration tests + env: + MAVEN_EXTRA_OPTS: >- + -Dopensearch.port=${{ matrix.port }} + -Delasticsearch.port=${{ matrix.port }} run: | - FLAGS="-Pintegration-tests" if [ "${{ matrix.search-engine }}" = "opensearch" ]; then - # Trigger OpenSearch profile activation via property; do not pass any -P profile toggles - FLAGS="$FLAGS -Duse.opensearch=true" + ./build.sh --ci --integration-tests --use-opensearch + else + ./build.sh --ci --integration-tests fi - mvn -ntp clean install $FLAGS \ - -Dopensearch.port=${{ matrix.port }} \ - -Delasticsearch.port=${{ matrix.port }} - name: Archive code coverage logs uses: actions/upload-artifact@v4 if: false # UNOMI-746 Reactivate if necessary From 88da78ad2be9f541b480e423a72021b85778e1b0 Mon Sep 17 00:00:00 2001 From: Serge Huber Date: Mon, 18 May 2026 15:04:49 +0200 Subject: [PATCH 20/21] UNOMI-921: Replace elasticsearch-maven-plugin with Docker-based Elasticsearch in integration tests Implements the core configuration switch from UNOMI-921: https://issues.apache.org/jira/browse/UNOMI-921 Replace the com.github.alexcojocaru:elasticsearch-maven-plugin (binary download + forked JVM) with io.fabric8:docker-maven-plugin in the elasticsearch profile of itests, mirroring how the opensearch profile already runs OpenSearch in a Docker container. itests/pom.xml (elasticsearch profile) * Add an 9400 property and pass it through the failsafe systemPropertyVariables so tests resolve the HTTP port from a single source (unchanged from the previous 9400). * Replace the elasticsearch-maven-plugin block with a docker-maven-plugin block that runs docker.elastic.co/elasticsearch/elasticsearch:${elasticsearch.test.version}, binds target/snapshots_repository to /tmp/snapshots_repository, and waits on the HTTP port before the integration-test phase. Heap aligned to 8GB (-Xms8g -Xmx8g) to match the OpenSearch configuration and the ES 9 recommendation. Discovery=single-node, replicas=0, xpack.ml and xpack.security disabled. * Container lifecycle matches OpenSearch exactly: pre-integration-test runs stop+remove then start (with showLogs); post-integration-test runs stop only -- container is kept around for inspection. * Add a chmod -R ugo+rwx on snapshots_repository in the antrun unzip step: the ES container runs as UID 1000, so on Linux CI the bind-mounted snapshot repo otherwise hits access_denied during repository verify. pom.xml (root) * Declare 0.48.0 and add the pluginManagement entry so the elasticsearch profile (and any future user of the plugin) inherits a single version. Scope kept minimal for the PR #757 stack split: only the test infrastructure switch lives here. The follow-up UNOMI-921 acceptance items below ship in the platform PR (P) once it lands: * Remove BaseIT.fixDefaultTemplateIfNeeded() and the call in checkSearchEngine() (no longer needed with Docker). * Migrate16xToCurrentVersionIT: replace hardcoded ES_BASE_URL = "http://localhost:9400" with dynamic getSearchPort(). * Drop the comments referring to the elasticsearch-maven-plugin template-override workaround. See docs/PR-757-stack-extraction-tracker.md for the full split plan and how this PR fits in the stack. --- itests/pom.xml | 91 ++++++++++++++++++++++++++++++++++---------------- pom.xml | 6 ++++ 2 files changed, 68 insertions(+), 29 deletions(-) diff --git a/itests/pom.xml b/itests/pom.xml index 98e98e9e56..0d0ccde8c5 100644 --- a/itests/pom.xml +++ b/itests/pom.xml @@ -196,6 +196,14 @@ + + + + + + @@ -210,6 +218,9 @@ elasticsearch + + 9400 + true @@ -229,6 +240,7 @@ foo elasticsearch + ${elasticsearch.port} @@ -247,43 +259,64 @@ - com.github.alexcojocaru - elasticsearch-maven-plugin - - 6.29 + io.fabric8 + docker-maven-plugin - - contextElasticSearchITests - 9500 - 9400 - ${elasticsearch.test.version} - true - 120 - - -Xms4g -Xmx4g - - - - false - ${project.build.directory}/snapshots_repository - false - OPTIONS,HEAD,GET,POST,PUT,DELETE - Authorization,X-Requested-With,X-Auth-Token,Content-Type,Content-Length - - + itests-elasticsearch + + + docker.elastic.co/elasticsearch/elasticsearch:${elasticsearch.test.version} + elasticsearch + + + ${elasticsearch.port}:9200 + + + single-node + -Xms8g -Xmx8g -Dcluster.default.index.settings.number_of_replicas=0 + false + false + /tmp/snapshots_repository + false + + + + ${project.build.directory}/snapshots_repository:/tmp/snapshots_repository + + + + + http://localhost:${elasticsearch.port} + GET + 200 + + + + ${project.build.directory}/elasticsearch-port.properties + + + - + + + remove-existing-container + pre-integration-test + + stop + remove + + + start-elasticsearch pre-integration-test - runforked + start + + true + stop-elasticsearch diff --git a/pom.xml b/pom.xml index 174e683081..f7e85f4035 100644 --- a/pom.xml +++ b/pom.xml @@ -146,6 +146,7 @@ 3.21.0 0.16.1 1.0-m5.1 + 0.48.0 v16.20.2 v1.22.19 @@ -867,6 +868,11 @@ dependency-check-maven ${dependency-check.plugin.version} + + io.fabric8 + docker-maven-plugin + ${docker-maven-plugin.version} + From eb22dd1fb59e3d153125cc669227be9d347bc96d Mon Sep 17 00:00:00 2001 From: Serge Huber Date: Tue, 2 Jun 2026 13:54:54 +0200 Subject: [PATCH 21/21] UNOMI-888: Remove duplicate Condition methods (merge artifact) --- .../unomi/api/conditions/Condition.java | 93 ------------------- 1 file changed, 93 deletions(-) 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 df4b2830d3..248d1c43a4 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 @@ -263,99 +263,6 @@ private Condition deepCopy(IdentityHashMap copying) { } } - /** - * 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 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 - */ - @Override - public Map toYaml(Set visited, int maxDepth) { - if (maxDepth <= 0) { - return YamlMapBuilder.create() - .put("type", conditionTypeId != null ? conditionTypeId : "Condition") - .put("parameterValues", "") - .build(); - } - if (visited != null && visited.contains(this)) { - return circularRef(); - } - final Set visitedSet = visited != null ? visited : YamlUtils.newIdentityVisitedSet(); - visitedSet.add(this); - try { - YamlMapBuilder builder = YamlMapBuilder.create() - .put("type", conditionTypeId != null ? conditionTypeId : "Condition"); - if (parameterValues != null && !parameterValues.isEmpty()) { - builder.put("parameterValues", toYamlValue(parameterValues, visitedSet, maxDepth - 1)); - } - return builder.build(); - } finally { - visitedSet.remove(this); - } - } - - /** - * 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 - * @throws IllegalStateException if the condition graph contains a cycle through nested {@link Condition} values - */ - public Condition deepCopy() { - return deepCopy(new IdentityHashMap<>()); - } - - private Condition deepCopy(IdentityHashMap copying) { - if (copying.put(this, Boolean.TRUE) != null) { - throw new IllegalStateException("Cyclic Condition graph: cannot deepCopy()"); - } - try { - Condition copied = new Condition(); - if (this.conditionType != null) { - copied.setConditionType(this.conditionType); - } else if (this.conditionTypeId != null) { - copied.setConditionTypeId(this.conditionTypeId); - } - - // Deep copy parameter values - Map copiedParams = new HashMap<>(); - if (this.parameterValues != null) { - for (Map.Entry entry : this.parameterValues.entrySet()) { - Object value = entry.getValue(); - if (value instanceof Condition) { - copiedParams.put(entry.getKey(), ((Condition) value).deepCopy(copying)); - } else if (value instanceof Collection) { - Collection collection = (Collection) value; - Collection copiedCollection; - if (collection instanceof List) { - copiedCollection = new ArrayList<>(); - } else { - copiedCollection = new ArrayList<>(); - } - for (Object item : collection) { - if (item instanceof Condition) { - copiedCollection.add(((Condition) item).deepCopy(copying)); - } else { - copiedCollection.add(item); - } - } - copiedParams.put(entry.getKey(), copiedCollection); - } else { - copiedParams.put(entry.getKey(), value); - } - } - } - copied.setParameterValues(copiedParams); - - return copied; - } finally { - copying.remove(this); - } - } - @Override public String toString() { Map map = toYaml();