package com.xfestudio.xfeservermanager.core.trigger;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
/** Validation and depth-first traversal for conditional trigger action trees. */
public final class TriggerActionTree {
public static final String CONDITION_TYPE = "condition";
public static final int MAX_NODES = TriggerProgramValidator.MAX_NODES;
public static final int MAX_NESTING_DEPTH = TriggerProgramValidator.MAX_NESTING_DEPTH;
private TriggerActionTree() { }
/**
* Validates the recursive action shape shared by visual documents and XFE Script.
*/
public static void validate(List<TriggerDefinition.Action> actions) {
Objects.requireNonNull(actions, "actions");
validateNodes(actions, 1, new Budget());
}
/** Returns every non-structural action in stable depth-first document order. */
public static List<TriggerDefinition.Action> leafActions(List<TriggerDefinition.Action> actions) {
validate(actions);
List<TriggerDefinition.Action> result = new ArrayList<>();
collectLeaves(actions, result);
return List.copyOf(result);
}
/**
* Returns executable actions whose enclosing condition nodes all match the event context.
* Structural condition nodes are never returned to a platform action executor.
*/
public static List<TriggerDefinition.Action> matchingActions(
List<TriggerDefinition.Action> actions, Map<String, ?> context) {
validate(actions);
List<TriggerDefinition.Action> result = new ArrayList<>();
collectMatching(actions, context == null ? Map.of() : context, result);
return List.copyOf(result);
}
/** Returns the normalized condition represented by a structural action node. */
public static TriggerDefinition.Condition condition(TriggerDefinition.Action action) {
Objects.requireNonNull(action, "action");
if (!CONDITION_TYPE.equals(action.type())) {
throw new IllegalArgumentException("action is not a condition node: " + action.type());
}
return new TriggerDefinition.Condition(
action.parameters().get("field"),
action.parameters().get("operator"),
action.parameters().getOrDefault("value", ""));
}
/**
* Validates a complete program while the public catalog still describes leaf actions only.
* This keeps old catalog callers compatible and gives catalog integrations a single migration
* point: validate every leaf and every structural condition recursively.
*/
public static void validateProgram(TriggerDefinition.Program program) {
Objects.requireNonNull(program, "program");
List<TriggerDefinition.Action> leaves = leafActions(program.actions());
if (leaves.isEmpty()) {
throw new IllegalArgumentException("a trigger needs at least one executable action");
}
TriggerCatalog.validate(new TriggerDefinition.Program(
program.event(), program.conditionMode(), program.conditions(), leaves));
List<TriggerDefinition.Condition> nestedConditions = new ArrayList<>();
collectConditions(program.actions(), nestedConditions);
if (!nestedConditions.isEmpty()) {
// A valid condition node always has an executable descendant, so the representative
// leaf also satisfies the catalog's non-empty action invariant.
TriggerCatalog.validate(new TriggerDefinition.Program(
program.event(), TriggerDefinition.MatchMode.ALL,
nestedConditions, List.of(leaves.get(0))));
}
}
private static void validateNodes(List<TriggerDefinition.Action> actions, int depth, Budget budget) {
if (depth > MAX_NESTING_DEPTH) {
throw new IllegalArgumentException("trigger action nesting exceeds " + MAX_NESTING_DEPTH);
}
for (TriggerDefinition.Action action : actions) {
if (action == null) throw new IllegalArgumentException("action must not be null");
if (++budget.nodes > MAX_NODES) {
throw new IllegalArgumentException("trigger action tree exceeds " + MAX_NODES + " nodes");
}
if (!CONDITION_TYPE.equals(action.type())) {
if (!action.children().isEmpty()) {
throw new IllegalArgumentException(
"only condition actions may contain child actions");
}
continue;
}
condition(action);
if (action.children().isEmpty()) {
throw new IllegalArgumentException("a condition action needs at least one child action");
}
validateNodes(action.children(), depth + 1, budget);
}
}
private static final class Budget { private int nodes; }
private static void collectLeaves(
List<TriggerDefinition.Action> actions, List<TriggerDefinition.Action> result) {
for (TriggerDefinition.Action action : actions) {
if (CONDITION_TYPE.equals(action.type())) {
collectLeaves(action.children(), result);
} else {
result.add(action);
}
}
}
private static void collectConditions(
List<TriggerDefinition.Action> actions, List<TriggerDefinition.Condition> result) {
for (TriggerDefinition.Action action : actions) {
if (!CONDITION_TYPE.equals(action.type())) continue;
result.add(condition(action));
collectConditions(action.children(), result);
}
}
private static void collectMatching(
List<TriggerDefinition.Action> actions,
Map<String, ?> context,
List<TriggerDefinition.Action> result) {
for (TriggerDefinition.Action action : actions) {
if (!CONDITION_TYPE.equals(action.type())) {
result.add(action);
continue;
}
if (TriggerEvaluator.matches(condition(action), context)) {
collectMatching(action.children(), context, result);
}
}
}
}
package com.xfestudio.xfeservermanager.core.trigger;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
/** Validation and depth-first traversal for conditional trigger action trees. */
public final class TriggerActionTree {
public static final String CONDITION_TYPE = "condition";
public static final int MAX_NODES = TriggerProgramValidator.MAX_NODES;
public static final int MAX_NESTING_DEPTH = TriggerProgramValidator.MAX_NESTING_DEPTH;
private TriggerActionTree() { }
/**
* Validates the recursive action shape shared by visual documents and XFE Script.
*/
public static void validate(List<TriggerDefinition.Action> actions) {
Objects.requireNonNull(actions, "actions");
validateNodes(actions, 1, new Budget());
}
/** Returns every non-structural action in stable depth-first document order. */
public static List<TriggerDefinition.Action> leafActions(List<TriggerDefinition.Action> actions) {
validate(actions);
List<TriggerDefinition.Action> result = new ArrayList<>();
collectLeaves(actions, result);
return List.copyOf(result);
}
/**
* Returns executable actions whose enclosing condition nodes all match the event context.
* Structural condition nodes are never returned to a platform action executor.
*/
public static List<TriggerDefinition.Action> matchingActions(
List<TriggerDefinition.Action> actions, Map<String, ?> context) {
validate(actions);
List<TriggerDefinition.Action> result = new ArrayList<>();
collectMatching(actions, context == null ? Map.of() : context, result);
return List.copyOf(result);
}
/** Returns the normalized condition represented by a structural action node. */
public static TriggerDefinition.Condition condition(TriggerDefinition.Action action) {
Objects.requireNonNull(action, "action");
if (!CONDITION_TYPE.equals(action.type())) {
throw new IllegalArgumentException("action is not a condition node: " + action.type());
}
return new TriggerDefinition.Condition(
action.parameters().get("field"),
action.parameters().get("operator"),
action.parameters().getOrDefault("value", ""));
}
/**
* Validates a complete program while the public catalog still describes leaf actions only.
* This keeps old catalog callers compatible and gives catalog integrations a single migration
* point: validate every leaf and every structural condition recursively.
*/
public static void validateProgram(TriggerDefinition.Program program) {
Objects.requireNonNull(program, "program");
List<TriggerDefinition.Action> leaves = leafActions(program.actions());
if (leaves.isEmpty()) {
throw new IllegalArgumentException("a trigger needs at least one executable action");
}
TriggerCatalog.validate(new TriggerDefinition.Program(
program.event(), program.conditionMode(), program.conditions(), leaves));
List<TriggerDefinition.Condition> nestedConditions = new ArrayList<>();
collectConditions(program.actions(), nestedConditions);
if (!nestedConditions.isEmpty()) {
// A valid condition node always has an executable descendant, so the representative
// leaf also satisfies the catalog's non-empty action invariant.
TriggerCatalog.validate(new TriggerDefinition.Program(
program.event(), TriggerDefinition.MatchMode.ALL,
nestedConditions, List.of(leaves.get(0))));
}
}
private static void validateNodes(List<TriggerDefinition.Action> actions, int depth, Budget budget) {
if (depth > MAX_NESTING_DEPTH) {
throw new IllegalArgumentException("trigger action nesting exceeds " + MAX_NESTING_DEPTH);
}
for (TriggerDefinition.Action action : actions) {
if (action == null) throw new IllegalArgumentException("action must not be null");
if (++budget.nodes > MAX_NODES) {
throw new IllegalArgumentException("trigger action tree exceeds " + MAX_NODES + " nodes");
}
if (!CONDITION_TYPE.equals(action.type())) {
if (!action.children().isEmpty()) {
throw new IllegalArgumentException(
"only condition actions may contain child actions");
}
continue;
}
condition(action);
if (action.children().isEmpty()) {
throw new IllegalArgumentException("a condition action needs at least one child action");
}
validateNodes(action.children(), depth + 1, budget);
}
}
private static final class Budget { private int nodes; }
private static void collectLeaves(
List<TriggerDefinition.Action> actions, List<TriggerDefinition.Action> result) {
for (TriggerDefinition.Action action : actions) {
if (CONDITION_TYPE.equals(action.type())) {
collectLeaves(action.children(), result);
} else {
result.add(action);
}
}
}
private static void collectConditions(
List<TriggerDefinition.Action> actions, List<TriggerDefinition.Condition> result) {
for (TriggerDefinition.Action action : actions) {
if (!CONDITION_TYPE.equals(action.type())) continue;
result.add(condition(action));
collectConditions(action.children(), result);
}
}
private static void collectMatching(
List<TriggerDefinition.Action> actions,
Map<String, ?> context,
List<TriggerDefinition.Action> result) {
for (TriggerDefinition.Action action : actions) {
if (!CONDITION_TYPE.equals(action.type())) {
result.add(action);
continue;
}
if (TriggerEvaluator.matches(condition(action), context)) {
collectMatching(action.children(), context, result);
}
}
}
}