Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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 @@ -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
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;
Comment thread
sergehuber marked this conversation as resolved.

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