Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
3ce73e7
UNOMI-920 Improve toString() methods with YAML formatting for debugging
sergehuber May 5, 2026
dd35f3e
Add manual dispatch to CodeQL Java workflow
sergehuber May 15, 2026
86d14d0
Add manual dispatch to CodeQL JavaScript workflow
sergehuber May 15, 2026
22de38c
CI: always publish IT JUnit report and update check on re-run
sergehuber May 15, 2026
bcf5a68
UNOMI-920: Address PR review feedback for YAML toString support
sergehuber May 16, 2026
c836baa
Merge branch 'master' into UNOMI-920-yaml-tostring
sergehuber May 16, 2026
df1dc2a
UNOMI-880 (split A0): migrate Elasticsearch integration tests to Docker
sergehuber May 18, 2026
cab5662
Merge branch 'UNOMI-itests-es-docker' into UNOMI-920-yaml-tostring
sergehuber May 18, 2026
a2e008c
UNOMI-921: Replace elasticsearch-maven-plugin with Docker-based Elast…
sergehuber May 18, 2026
30c444e
Merge branch 'UNOMI-itests-es-docker' into UNOMI-920-yaml-tostring
sergehuber May 18, 2026
2f991d0
Merge branch 'master' into UNOMI-itests-es-docker
sergehuber May 19, 2026
2f881c4
Merge branch 'UNOMI-itests-es-docker' into UNOMI-920-yaml-tostring
sergehuber May 19, 2026
944c39b
UNOMI-937: Add build.sh CI mode for non-interactive GitHub Actions
sergehuber May 23, 2026
eb294da
UNOMI-937: Harden flaky integration tests with keepTrying polling
sergehuber May 23, 2026
c2f3001
UNOMI-937: Add waitForProfileProperty helper for async rule updates
sergehuber May 23, 2026
2611dea
UNOMI-937: Run GitHub Actions unit and IT jobs via build.sh
sergehuber May 23, 2026
88da78a
UNOMI-921: Replace elasticsearch-maven-plugin with Docker-based Elast…
sergehuber May 18, 2026
983e6a2
Merge remote-tracking branch 'origin/UNOMI-itests-es-docker' into UNO…
sergehuber May 23, 2026
dea4cd0
Merge branch 'UNOMI-921-itests-es-docker' into UNOMI-920-yaml-tostring
sergehuber May 23, 2026
899daf1
Merge branch 'master' into UNOMI-921-itests-es-docker
sergehuber May 28, 2026
749c5ae
Merge branch 'UNOMI-921-itests-es-docker' into UNOMI-920-yaml-tostring
sergehuber May 28, 2026
45782b7
Merge branch 'master' into UNOMI-920-yaml-tostring
sergehuber May 29, 2026
6e1221a
Fix GraphQLListIT compilation: restore LOGGER and catch block lost in…
sergehuber May 29, 2026
934c969
Fix YamlUtils thread safety, Condition null-safety, depth decrement, …
sergehuber May 29, 2026
ad4f1bd
Fix ConditionTest: expect LinkedHashSet after deepCopy of Set paramet…
sergehuber May 29, 2026
f3af2ce
Revert testUpdateProperties_CurrentProfile to in-memory assertion
sergehuber May 29, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/codeql-analysis-java.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand Down
1 change: 1 addition & 0 deletions .github/workflows/codeql-analysis-javascript.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand Down
9 changes: 8 additions & 1 deletion .github/workflows/unomi-ci-build-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -85,8 +85,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
24 changes: 24 additions & 0 deletions api/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -66,13 +66,37 @@
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>${slf4j.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.yaml</groupId>
<artifactId>snakeyaml</artifactId>
</dependency>

<!-- Test Dependencies -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<scope>test</scope>
</dependency>
<!-- SLF4J Implementation for Testing -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>${slf4j.version}</version>
<scope>test</scope>
</dependency>
</dependencies>

<reporting>
Expand Down
65 changes: 61 additions & 4 deletions api/src/main/java/org/apache/unomi/api/Item.java
Original file line number Diff line number Diff line change
Expand Up @@ -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 <strong>must</strong> define a
Expand All @@ -36,10 +40,13 @@
* 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;
/**
* 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<Class,String> itemTypeCache = new ConcurrentHashMap<>();

Expand Down Expand Up @@ -150,4 +157,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<String, Object> toYaml(Set<Object> visited, int maxDepth) {
if (maxDepth <= 0) {
return YamlMapBuilder.create()
.put("itemId", itemId)
.put("itemType", itemType)
.put("systemMetadata", "<max depth exceeded>")
.build();
}
final Set<Object> visitedSet = visited != null ? visited : 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,
// 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<String, Object> map = toYaml();
return YamlUtils.format(map);
}
}
45 changes: 44 additions & 1 deletion api/src/main/java/org/apache/unomi/api/Metadata.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,23 @@

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.LinkedHashSet;
import java.util.Map;
import java.util.Set;

import static org.apache.unomi.api.utils.YamlUtils.circularRef;

/**
* A class providing information about context server entities.
*
* @see MetadataItem
*/
public class Metadata implements Comparable<Metadata>, Serializable {
public class Metadata implements Comparable<Metadata>, Serializable, YamlConvertible {

private static final long serialVersionUID = 7446061538573517071L;

Expand Down Expand Up @@ -279,5 +286,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<String, Object> toYaml(Set<Object> visited, int maxDepth) {
if (visited != null && visited.contains(this)) {
return circularRef();
}
final Set<Object> visitedSet = visited != null ? visited : YamlUtils.newIdentityVisitedSet();
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<String, Object> map = toYaml();
return YamlUtils.format(map);
}
}
56 changes: 54 additions & 2 deletions api/src/main/java/org/apache/unomi/api/MetadataItem.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,15 @@

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.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}.
Expand All @@ -31,7 +38,7 @@ public MetadataItem() {
}

public MetadataItem(Metadata metadata) {
super(metadata.getId());
super(metadata != null ? metadata.getId() : null);
this.metadata = metadata;
}

Expand All @@ -54,7 +61,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<String, Object> toYaml(Set<Object> visited, int maxDepth) {
if (maxDepth <= 0) {
return YamlMapBuilder.create()
.put("metadata", "<max depth exceeded>")
.build();
}
final Set<Object> visitedSet = visited != null ? visited : 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);
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<String, Object> map = toYaml();
return YamlUtils.format(map);
}
}
Loading
Loading