Skip to content

Commit 9675e2b

Browse files
Merge pull request #158 from OP-TED/TEDEFO-4766-dynamic-rules
TEDEFO 4766 dynamic rules
2 parents f2e1a58 + 2093f49 commit 9675e2b

161 files changed

Lines changed: 4650 additions & 550 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

src/main/java/eu/europa/ted/eforms/sdk/schematron/SchematronAssert.java

Lines changed: 102 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2022 European Union
2+
* Copyright 2025 European Union
33
*
44
* Licensed under the EUPL, Version 1.2 or – as soon they will be approved by the European
55
* Commission – subsequent versions of the EUPL (the "Licence"); You may not use this work except in
@@ -13,21 +13,121 @@
1313
*/
1414
package eu.europa.ted.eforms.sdk.schematron;
1515

16+
import java.util.List;
17+
1618
import eu.europa.ted.efx.model.Context;
19+
import eu.europa.ted.efx.model.rules.RuleNature;
20+
import eu.europa.ted.efx.model.rules.RuleSeverity;
1721
import eu.europa.ted.efx.model.rules.ValidationRule;
22+
import eu.europa.ted.efx.model.variables.DynamicVariable;
1823

1924
/**
2025
* Represents a Schematron <assert> element.
2126
* Fires when the test expression evaluates to false.
27+
* For rules referencing dynamic variables, the test is guarded so that API errors
28+
* do not cause the main rule to fire — a companion {@link NoApiError} assert handles that.
2229
*/
2330
public class SchematronAssert extends SchematronTest {
2431

25-
public SchematronAssert(ValidationRule rule, Context ruleContext) {
32+
private final List<DynamicVariable> dynamicVariables;
33+
34+
public SchematronAssert(final ValidationRule rule, final Context ruleContext) {
2635
super(rule, ruleContext);
36+
this.dynamicVariables = rule.findReferencedDynamicVariables();
37+
}
38+
39+
@Override
40+
public RuleNature getRuleNature() {
41+
if (!this.dynamicVariables.isEmpty()) {
42+
return RuleNature.DYNAMIC;
43+
}
44+
return super.getRuleNature();
2745
}
2846

2947
@Override
3048
public String getElementName() {
3149
return "assert";
3250
}
51+
52+
@Override
53+
public String getTest() {
54+
String baseTest = super.getTest();
55+
if (this.getRuleNature() != RuleNature.DYNAMIC) {
56+
return baseTest;
57+
}
58+
// An assert fires when the test is false. Prepending "($varName = -1) or" makes the
59+
// test true when any dynamic variable errored, preventing the main assert from firing.
60+
// Companion NoApiError asserts handle API errors separately.
61+
StringBuilder sb = new StringBuilder();
62+
for (var dynamicVar : this.dynamicVariables) {
63+
sb.append("($").append(dynamicVar.name).append(" = -1) or ");
64+
}
65+
for (var variable : this.rule.getAutoGeneratedVariables()) {
66+
sb.append("($").append(variable.name).append(" = -1) or ");
67+
}
68+
if (this.rule.getCondition() != null) {
69+
// WHEN clause: combineWithOrParenthesized already wrapped each operand in parens.
70+
sb.append(baseTest);
71+
} else {
72+
// No WHEN clause: the raw expression needs wrapping to isolate it from the guards.
73+
sb.append("(").append(baseTest).append(")");
74+
}
75+
return sb.toString();
76+
}
77+
78+
/**
79+
* A companion Schematron &lt;assert&gt; that checks a dynamic variable did not return an error (-1).
80+
* Generated for each dynamic variable (declared or auto-generated) referenced by a rule.
81+
*/
82+
public static class NoApiError extends SchematronAssert {
83+
84+
private final DynamicVariable dynamicVariable;
85+
86+
public NoApiError(ValidationRule rule, DynamicVariable dynamicVariable, Context ruleContext) {
87+
super(rule, ruleContext);
88+
this.dynamicVariable = dynamicVariable;
89+
}
90+
91+
@Override
92+
public RuleNature getRuleNature() {
93+
return RuleNature.DYNAMIC;
94+
}
95+
96+
@Override
97+
public SchematronLet getLetElement() {
98+
if (this.dynamicVariable instanceof DynamicVariable.AutoGenerated) {
99+
return new SchematronLet(this.dynamicVariable);
100+
}
101+
return null;
102+
}
103+
104+
@Override
105+
public String getId() {
106+
return this.rule.getId() + "-api-error-" + this.dynamicVariable.identity();
107+
}
108+
109+
@Override
110+
public String getRole() {
111+
return this.dynamicVariable.errorSeverity().toString().toUpperCase();
112+
}
113+
114+
@Override
115+
public String getTest() {
116+
return "not($" + this.dynamicVariable.name + " = -1)";
117+
}
118+
119+
@Override
120+
public String getMessage() {
121+
if (this.dynamicVariable.errorLabel() != null) {
122+
return this.dynamicVariable.errorLabel();
123+
}
124+
return this.dynamicVariable.errorSeverity() == RuleSeverity.WARNING
125+
? "rule|text|api-warning" : "rule|text|api-error";
126+
}
127+
128+
@Override
129+
public SchematronDiagnostic getDiagnostic() {
130+
return null;
131+
}
132+
}
33133
}

src/main/java/eu/europa/ted/eforms/sdk/schematron/SchematronDiagnostic.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2022 European Union
2+
* Copyright 2025 European Union
33
*
44
* Licensed under the EUPL, Version 1.2 or – as soon they will be approved by the European
55
* Commission – subsequent versions of the EUPL (the "Licence"); You may not use this work except in

src/main/java/eu/europa/ted/eforms/sdk/schematron/SchematronGenerator.java

Lines changed: 64 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2022 European Union
2+
* Copyright 2025 European Union
33
*
44
* Licensed under the EUPL, Version 1.2 or – as soon they will be approved by the European
55
* Commission – subsequent versions of the EUPL (the "Licence"); You may not use this work except in
@@ -20,6 +20,7 @@
2020
import java.util.LinkedHashMap;
2121
import java.util.List;
2222
import java.util.Map;
23+
import java.util.Set;
2324
import java.util.stream.Collectors;
2425

2526
import org.slf4j.Logger;
@@ -32,7 +33,8 @@
3233
import eu.europa.ted.eforms.sdk.component.SdkComponent;
3334
import eu.europa.ted.eforms.sdk.component.SdkComponentType;
3435
import eu.europa.ted.efx.interfaces.ValidatorGenerator;
35-
import eu.europa.ted.efx.model.rules.CompleteValidation;
36+
import eu.europa.ted.efx.model.rules.ValidationPlan;
37+
import eu.europa.ted.efx.model.rules.RuleNature;
3638
import eu.europa.ted.efx.model.rules.ValidationStage;
3739
import eu.europa.ted.efx.model.variables.Variable;
3840
import freemarker.template.Configuration;
@@ -69,31 +71,59 @@ public SchematronGenerator() {
6971
// #region ValidatorMarkupGenerator Implementation
7072

7173
@Override
72-
public Map<String, String> generateOutput(CompleteValidation completeValidation) {
73-
logger.debug("Generating Schematron output from {} stages", completeValidation.getStages().size());
74+
public Map<String, String> generateOutput(ValidationPlan validationPlan) {
75+
logger.debug("Generating Schematron output from {} stages", validationPlan.getStages().size());
7476

7577
// Create local state for this generation run
7678
SchematronSchema schema = new SchematronSchema("eForms schematron rules");
7779
List<SchematronPattern> patterns = new ArrayList<>();
7880
Map<String, SchematronDiagnostic> diagnosticsMap = new LinkedHashMap<>();
7981

82+
// Add endpoint params to schema
83+
for (Map.Entry<String, String> endpoint : validationPlan.getEndpoints().entrySet()) {
84+
String url = endpoint.getValue() != null ? endpoint.getValue() : "";
85+
SchematronParam param = new SchematronParam("apiUrl-" + endpoint.getKey(), "'" + url + "'");
86+
schema.addParam(param);
87+
logger.debug("Added endpoint param: {} = {}", param.getName(), param.getValue());
88+
}
89+
8090
// Add global variables to schema
81-
for (Variable variable : completeValidation.getGlobalVariables()) {
82-
String xpathValue = variable.initializationExpression.getScript();
83-
SchematronLet globalVar = new SchematronLet(variable.name, xpathValue);
84-
schema.addGlobalVariable(globalVar);
85-
logger.debug("Added global variable: {} = {}", variable.name, xpathValue);
91+
for (Variable variable : validationPlan.getVariables()) {
92+
SchematronLet letElement = new SchematronLet(variable);
93+
schema.addLetElement(letElement);
94+
logger.debug("Added global variable: {} = {}", variable.name, variable.initializationExpression.getScript());
8695
}
8796

8897
// Transform intermediate model (ValidationStage) to Schematron model (SchematronPattern)
89-
transformStagesToPatterns(completeValidation.getStages(), patterns, diagnosticsMap);
98+
for (ValidationStage stage : validationPlan.getStages()) {
99+
if (stage.containsUniversalRules()) {
100+
SchematronPattern sharedPattern = new SchematronPattern(stage);
101+
if (sharedPattern.hasRules()) {
102+
patterns.add(sharedPattern);
103+
diagnosticsMap.putAll(sharedPattern.getDiagnostics());
104+
logger.debug("Created shared pattern {} for stage {}",
105+
sharedPattern.getId(), stage.getName());
106+
}
107+
}
108+
for (String noticeSubtype : stage.getNoticeSubtypes()) {
109+
SchematronPattern pattern = new SchematronPattern(stage, noticeSubtype);
110+
if (pattern.hasRules()) {
111+
patterns.add(pattern);
112+
diagnosticsMap.putAll(pattern.getDiagnostics());
113+
logger.debug("Created pattern {} for stage {} / notice subtype {}",
114+
pattern.getId(), stage.getName(), noticeSubtype);
115+
}
116+
}
117+
}
90118

91119
// Add collected diagnostics to schema
92-
addDiagnosticsToSchema(diagnosticsMap, schema);
120+
for (SchematronDiagnostic diagnostic : diagnosticsMap.values()) {
121+
schema.addDiagnostic(diagnostic);
122+
}
93123

94124
// Generate all output files
95125
try {
96-
return generateOutputFiles(completeValidation.getNoticeSubtypes(), patterns, schema);
126+
return this.generateOutputFiles(validationPlan.getNoticeSubtypes(), patterns, schema);
97127
} catch (IOException e) {
98128
throw new RuntimeException("Failed to generate Schematron output", e);
99129
}
@@ -135,61 +165,20 @@ public String generatePattern(SchematronPattern pattern, SchematronOutputConfig
135165

136166
Map<String, Object> model = new HashMap<>();
137167
model.put("id", pattern.getId());
138-
model.put("variables", pattern.getVariables());
168+
List<String> tags = config.ruleNatures().stream()
169+
.map(Enum::name).collect(Collectors.toList());
170+
List<SchematronLet> letElements = pattern.getLetElements().stream()
171+
.filter(v -> tags.contains(v.getTag())).collect(Collectors.toList());
172+
model.put("letElements", letElements);
139173
model.put("rules", pattern.getRules());
140-
model.put("tags", config.ruleNatures().stream()
141-
.map(Enum::name)
142-
.collect(Collectors.toList()));
174+
model.put("tags", tags);
143175

144176
template.process(model, writer);
145177
return writer.toString();
146178
}
147179

148180
// #endregion Freemarker Template Methods
149181

150-
// #region Transformation Methods
151-
152-
/**
153-
* Transforms validation stages into Schematron patterns.
154-
* For each stage, first creates a shared pattern for rules that apply to all subtypes,
155-
* then creates subtype-specific patterns for the remaining rules.
156-
*/
157-
private void transformStagesToPatterns(List<ValidationStage> stages,
158-
List<SchematronPattern> patterns, Map<String, SchematronDiagnostic> diagnosticsMap) {
159-
for (ValidationStage stage : stages) {
160-
// Create shared pattern for rules that apply to all subtypes
161-
if (stage.containsUniversalRules()) {
162-
SchematronPattern sharedPattern = new SchematronPattern(stage);
163-
if (sharedPattern.hasRules()) {
164-
patterns.add(sharedPattern);
165-
diagnosticsMap.putAll(sharedPattern.getDiagnostics());
166-
logger.debug("Created shared pattern {} for stage {}",
167-
sharedPattern.getId(), stage.getName());
168-
}
169-
}
170-
171-
// Create subtype-specific patterns for remaining rules
172-
for (String noticeSubtype : stage.getNoticeSubtypes()) {
173-
SchematronPattern pattern = new SchematronPattern(stage, noticeSubtype);
174-
if (pattern.hasRules()) {
175-
patterns.add(pattern);
176-
diagnosticsMap.putAll(pattern.getDiagnostics());
177-
logger.debug("Created pattern {} for stage {} / notice subtype {}",
178-
pattern.getId(), stage.getName(), noticeSubtype);
179-
}
180-
}
181-
}
182-
}
183-
184-
private void addDiagnosticsToSchema(Map<String, SchematronDiagnostic> diagnosticsMap,
185-
SchematronSchema schema) {
186-
for (SchematronDiagnostic diagnostic : diagnosticsMap.values()) {
187-
schema.addDiagnostic(diagnostic);
188-
}
189-
}
190-
191-
// #endregion Transformation Methods
192-
193182
// #region Output Generation Methods
194183

195184
/**
@@ -219,11 +208,11 @@ private Map<String, String> generateOutputFiles(List<String> noticeSubtypeIds,
219208

220209
try {
221210
for (SchematronOutputConfig config : configs) {
222-
generateOutputForConfig(config, noticeSubtypeIds, patterns, baseSchema, outputFiles, schematronsMetadata);
211+
this.generateOutputForConfig(config, noticeSubtypeIds, patterns, baseSchema, outputFiles, schematronsMetadata);
223212
}
224213

225214
// Generate schematrons.json with entries from all configurations
226-
String schematronsJson = generateSchematronsJson(schematronsMetadata);
215+
String schematronsJson = this.generateSchematronsJson(schematronsMetadata);
227216
outputFiles.put("schematrons.json", schematronsJson);
228217

229218
logger.debug("Generated {} Schematron files", outputFiles.size());
@@ -251,8 +240,18 @@ private void generateOutputForConfig(
251240

252241
// Create a fresh schema for this configuration
253242
SchematronSchema schema = new SchematronSchema(baseSchema.getTitle());
254-
for (SchematronLet globalVar : baseSchema.getGlobalVariables()) {
255-
schema.addGlobalVariable(globalVar);
243+
// API endpoint params are only relevant for configurations that include dynamic rules
244+
if (config.ruleNatures().contains(RuleNature.DYNAMIC)) {
245+
for (SchematronParam param : baseSchema.getParams()) {
246+
schema.addParam(param);
247+
}
248+
}
249+
Set<String> configTags = config.ruleNatures().stream()
250+
.map(Enum::name).collect(Collectors.toSet());
251+
for (SchematronLet letElement : baseSchema.getLetElements()) {
252+
if (configTags.contains(letElement.getTag())) {
253+
schema.addLetElement(letElement);
254+
}
256255
}
257256
for (SchematronDiagnostic diagnostic : baseSchema.getDiagnostics()) {
258257
schema.addDiagnostic(diagnostic);
@@ -307,9 +306,9 @@ private void generateOutputForConfig(
307306
}
308307

309308
// Generate complete-validation.sch for this configuration
310-
String completeValidation = generateCompleteValidation(schema);
309+
String completeValidationContent = generateCompleteValidation(schema);
311310
String completeFilename = folderPrefix + "complete-validation.sch";
312-
outputFiles.put(completeFilename, completeValidation);
311+
outputFiles.put(completeFilename, completeValidationContent);
313312

314313
// Add complete-validation to metadata at the beginning of this config's entries
315314
Map<String, Object> completeMetadata = new LinkedHashMap<>();

src/main/java/eu/europa/ted/eforms/sdk/schematron/SchematronLet.java

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2022 European Union
2+
* Copyright 2025 European Union
33
*
44
* Licensed under the EUPL, Version 1.2 or – as soon they will be approved by the European
55
* Commission – subsequent versions of the EUPL (the "Licence"); You may not use this work except in
@@ -13,16 +13,32 @@
1313
*/
1414
package eu.europa.ted.eforms.sdk.schematron;
1515

16+
import eu.europa.ted.efx.model.rules.RuleNature;
17+
import eu.europa.ted.efx.model.variables.DynamicVariable;
18+
import eu.europa.ted.efx.model.variables.Variable;
19+
1620
/**
1721
* Represents a Schematron &lt;let&gt; element for variable declarations.
1822
*/
1923
public class SchematronLet {
2024
private final String name;
2125
private final String value;
26+
private final RuleNature nature;
27+
28+
public SchematronLet(final Variable variable) {
29+
this(variable.name, variable.initializationExpression.getScript(),
30+
variable instanceof DynamicVariable || variable.hasDynamicDependencies()
31+
? RuleNature.DYNAMIC : RuleNature.STATIC);
32+
}
2233

2334
public SchematronLet(String name, String value) {
35+
this(name, value, RuleNature.STATIC);
36+
}
37+
38+
public SchematronLet(String name, String value, RuleNature nature) {
2439
this.name = name;
2540
this.value = value;
41+
this.nature = nature;
2642
}
2743

2844
/** Used by pattern.ftl and complete-validation.ftl */
@@ -34,4 +50,9 @@ public String getName() {
3450
public String getValue() {
3551
return this.value;
3652
}
53+
54+
/** Used by pattern.ftl — returns the tag for filtering (derived from nature) */
55+
public String getTag() {
56+
return this.nature.name();
57+
}
3758
}

0 commit comments

Comments
 (0)