Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -151,20 +151,31 @@ public String generatePattern(SchematronPattern pattern, SchematronOutputConfig

/**
* Transforms validation stages into Schematron patterns.
* Creates one pattern per (stage, noticeType) combination, where each pattern
* only contains the rules/assertions that apply to that specific notice type.
* For each stage, first creates a shared pattern for rules that apply to all subtypes,
* then creates subtype-specific patterns for the remaining rules.
*/
private void transformStagesToPatterns(List<ValidationStage> stages,
List<SchematronPattern> patterns, Map<String, SchematronDiagnostic> diagnosticsMap) {
for (ValidationStage stage : stages) {
// Get all notice types referenced in this stage
for (String noticeType : SchematronPattern.getNoticeTypesInStage(stage)) {
SchematronPattern pattern = new SchematronPattern(stage, noticeType);
// Create shared pattern for rules that apply to all subtypes
if (stage.containsUniversalRules()) {
SchematronPattern sharedPattern = new SchematronPattern(stage);
if (sharedPattern.hasRules()) {
patterns.add(sharedPattern);
diagnosticsMap.putAll(sharedPattern.getDiagnostics());
logger.debug("Created shared pattern {} for stage {}",
sharedPattern.getId(), stage.getName());
}
}

// Create subtype-specific patterns for remaining rules
for (String noticeSubtype : stage.getNoticeSubtypes()) {
SchematronPattern pattern = new SchematronPattern(stage, noticeSubtype);
if (pattern.hasRules()) {
patterns.add(pattern);
diagnosticsMap.putAll(pattern.getDiagnostics());
logger.debug("Created pattern {} for stage {} / notice type {}",
pattern.getId(), stage.getName(), noticeType);
logger.debug("Created pattern {} for stage {} / notice subtype {}",
pattern.getId(), stage.getName(), noticeSubtype);
}
}
}
Expand Down Expand Up @@ -193,7 +204,7 @@ private void addDiagnosticsToSchema(Map<String, SchematronDiagnostic> diagnostic
* @return A map of filename to file content for all generated Schematron files
* @throws IOException If an error occurs during file generation
*/
private Map<String, String> generateOutputFiles(List<String> noticeTypeIds,
private Map<String, String> generateOutputFiles(List<String> noticeSubtypeIds,
List<SchematronPattern> patterns, SchematronSchema baseSchema) throws IOException {
logger.debug("Generating Schematron output files");

Expand All @@ -208,7 +219,7 @@ private Map<String, String> generateOutputFiles(List<String> noticeTypeIds,

try {
for (SchematronOutputConfig config : configs) {
generateOutputForConfig(config, noticeTypeIds, patterns, baseSchema, outputFiles, schematronsMetadata);
generateOutputForConfig(config, noticeSubtypeIds, patterns, baseSchema, outputFiles, schematronsMetadata);
}

// Generate schematrons.json with entries from all configurations
Expand All @@ -229,7 +240,7 @@ private Map<String, String> generateOutputFiles(List<String> noticeTypeIds,
*/
private void generateOutputForConfig(
SchematronOutputConfig config,
List<String> noticeTypeIds,
List<String> noticeSubtypeIds,
List<SchematronPattern> patterns,
SchematronSchema baseSchema,
Map<String, String> outputFiles,
Expand Down Expand Up @@ -280,13 +291,13 @@ private void generateOutputForConfig(
}

// Build and add phases to schema
Map<String, List<String>> phasesMap = buildPhasesMapForConfig(noticeTypeIds, patterns, config);
Map<String, List<String>> phasesMap = buildPhasesMapForConfig(noticeSubtypeIds, patterns, config);
for (Map.Entry<String, List<String>> entry : phasesMap.entrySet()) {
String noticeTypeId = entry.getKey();
String noticeSubtypeId = entry.getKey();
List<String> patternIds = entry.getValue();

if (!patternIds.isEmpty()) {
SchematronPhase phase = new SchematronPhase(noticeTypeId);
SchematronPhase phase = new SchematronPhase(noticeSubtypeId);
for (String patternId : patternIds) {
phase.addActivePattern(patternId);
}
Expand Down Expand Up @@ -314,33 +325,41 @@ private void generateOutputForConfig(
}

/**
* Builds a map from notice type ID to list of pattern IDs that apply to it,
* Builds a map from notice subtype ID to list of pattern IDs that apply to it,
* filtered by the given output configuration.
* Each pattern now applies to exactly one notice type, so this is a simple grouping.
* Pattern order is preserved from the EFX file order.
* Shared patterns (applying to all subtypes) are added to every phase.
* Subtype-specific patterns are added only to their notice subtype's phase.
*
* @param config The output configuration specifying which rule natures to include
* @return Map of notice type ID to ordered list of pattern IDs
* @return Map of notice subtype ID to ordered list of pattern IDs
*/
private Map<String, List<String>> buildPhasesMapForConfig(List<String> noticeTypeIds,
private Map<String, List<String>> buildPhasesMapForConfig(List<String> noticeSubtypeIds,
List<SchematronPattern> patterns, SchematronOutputConfig config) {
Map<String, List<String>> phasesMap = new LinkedHashMap<>();

// Initialize map with all valid notice types
for (String noticeTypeId : noticeTypeIds) {
phasesMap.put(noticeTypeId, new ArrayList<>());
// Initialize map with all valid notice subtypes
for (String noticeSubtypeId : noticeSubtypeIds) {
phasesMap.put(noticeSubtypeId, new ArrayList<>());
}

// Each pattern applies to exactly one notice type
for (SchematronPattern pattern : patterns) {
// Skip patterns that don't have rules matching this configuration
if (!pattern.hasRulesFor(config.ruleNatures())) {
continue;
}
String noticeType = pattern.getNoticeType();
List<String> patternList = phasesMap.get(noticeType);
if (patternList != null) {
patternList.add(pattern.getId());

if (pattern.isShared()) {
// Shared patterns go into all phases
for (List<String> patternList : phasesMap.values()) {
patternList.add(pattern.getId());
}
} else {
// Subtype-specific patterns go only into their notice subtype's phase
String noticeSubtype = pattern.getNoticeSubtype();
List<String> patternList = phasesMap.get(noticeSubtype);
if (patternList != null) {
patternList.add(pattern.getId());
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,40 +15,50 @@

import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import eu.europa.ted.efx.model.rules.RuleNature;
import eu.europa.ted.efx.model.rules.RuleSet;
import eu.europa.ted.efx.model.rules.ValidationRule;
import eu.europa.ted.efx.model.rules.ValidationStage;
import eu.europa.ted.efx.model.variables.Variable;

/**
* Represents a Schematron &lt;pattern&gt; element.
* Each pattern is specific to a stage AND a notice type, containing only
* the assertions that apply to that notice type.
* A pattern can be either subtype-specific (for a single notice subtype) or shared
* (for rules that apply to all notice subtypes).
*/
public class SchematronPattern {
private final String stage;
private final String noticeType;
private final String noticeSubtype;
private final List<SchematronLet> variables;
private final List<SchematronRule> rules;

/**
* Creates a pattern for a specific stage and notice type combination.
* Only includes rules/assertions that apply to the given notice type.
* Creates a shared pattern for a stage, containing only rules that apply to all subtypes.
*
* @param validationStage The validation stage
* @param noticeType The notice type to filter by
*/
public SchematronPattern(ValidationStage validationStage, String noticeType) {
public SchematronPattern(ValidationStage validationStage) {
this.stage = validationStage.getName();
this.noticeType = noticeType;
this.noticeSubtype = null;
this.variables = collectVariables(validationStage);
this.rules = createRules(validationStage, noticeType);
this.rules = createUniversalRules(validationStage);
}

/**
* Creates a pattern for a specific stage and notice subtype combination.
* Only includes subtype-specific rules; rules that apply to all subtypes are excluded.
*
* @param validationStage The validation stage
* @param noticeSubtype The notice subtype to filter by
*/
public SchematronPattern(ValidationStage validationStage, String noticeSubtype) {
this.stage = validationStage.getName();
this.noticeSubtype = noticeSubtype;
this.variables = collectVariables(validationStage);
this.rules = createSubtypeSpecificRules(validationStage, noticeSubtype);
}

private static List<SchematronLet> collectVariables(ValidationStage stage) {
Expand All @@ -64,48 +74,47 @@ private static List<SchematronLet> collectVariables(ValidationStage stage) {
return vars;
}

private static List<SchematronRule> createRules(ValidationStage stage, String noticeType) {
private static List<SchematronRule> createUniversalRules(ValidationStage stage) {
List<SchematronRule> rules = new ArrayList<>();
for (RuleSet ruleSet : stage.getRuleSets()) {
SchematronRule rule = new SchematronRule(ruleSet, noticeType);
SchematronRule rule = SchematronRule.createUniversalRule(ruleSet);
if (rule.hasTests()) {
rules.add(rule);
}
}
return rules;
}

/**
* Returns all notice types referenced in the given stage.
* Used by SchematronGenerator to determine which patterns to create.
*/
public static Set<String> getNoticeTypesInStage(ValidationStage stage) {
Set<String> types = new LinkedHashSet<>();
private static List<SchematronRule> createSubtypeSpecificRules(ValidationStage stage, String noticeSubtype) {
List<SchematronRule> rules = new ArrayList<>();
for (RuleSet ruleSet : stage.getRuleSets()) {
for (ValidationRule rule : ruleSet) {
if (rule.getNoticeSubtypes() != null) {
types.addAll(rule.getNoticeSubtypes().asList());
}
}
ValidationRule fallback = ruleSet.getFallbackRule();
if (fallback != null && fallback.getNoticeSubtypes() != null) {
types.addAll(fallback.getNoticeSubtypes().asList());
SchematronRule rule = SchematronRule.createSubtypeSpecificRule(ruleSet, noticeSubtype);
if (rule.hasTests()) {
rules.add(rule);
}
}
return types;
return rules;
}

/** Used by pattern.ftl */
public String getId() {
return "validation-stage-" + this.stage + "-" + this.noticeType;
if (this.noticeSubtype == null) {
return "validation-stage-" + this.stage;
}
return "validation-stage-" + this.stage + "-" + this.noticeSubtype;
}

public String getStage() {
return this.stage;
}

public String getNoticeType() {
return this.noticeType;
public String getNoticeSubtype() {
return this.noticeSubtype;
}

/** Returns true if this is a shared pattern (applies to all notice subtypes). */
public boolean isShared() {
return this.noticeSubtype == null;
}

/** Used by pattern.ftl */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,16 +20,16 @@
* Represents a Schematron &lt;phase&gt; element.
*/
public class SchematronPhase {
private final String noticeType;
private final String noticeSubtype;
private final List<String> activePatterns = new ArrayList<>();

public SchematronPhase(String noticeType) {
this.noticeType = noticeType;
public SchematronPhase(String noticeSubtype) {
this.noticeSubtype = noticeSubtype;
}

/** Used by complete-validation.ftl */
public String getId() {
return "eforms-" + this.noticeType;
return "eforms-" + this.noticeSubtype;
}

/** Used by complete-validation.ftl */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Predicate;

import eu.europa.ted.efx.model.Context;
import eu.europa.ted.efx.model.rules.RuleNature;
Expand All @@ -35,12 +36,22 @@ public class SchematronRule {
private final Context context;

/**
* Creates a SchematronRule containing only tests that apply to the given notice type.
*
* @param ruleSet The source rule set
* @param noticeType The notice type to filter tests by
* Creates a SchematronRule for rules that apply to all notice subtypes (shared pattern).
*/
public static SchematronRule createUniversalRule(RuleSet ruleSet) {
return new SchematronRule(ruleSet, SchematronRule::isUniversal);
}

/**
* Creates a SchematronRule for rules specific to a single notice subtype.
* Excludes rules that apply to all subtypes (those go into the shared pattern).
*/
public SchematronRule(RuleSet ruleSet, String noticeType) {
public static SchematronRule createSubtypeSpecificRule(RuleSet ruleSet, String noticeSubtype) {
return new SchematronRule(ruleSet,
rule -> !isUniversal(rule) && appliesToNoticeSubtype(rule, noticeSubtype));
}

private SchematronRule(RuleSet ruleSet, Predicate<ValidationRule> filter) {
this.context = ruleSet.getContext();

List<SchematronLet> variables = new ArrayList<>();
Expand All @@ -51,7 +62,7 @@ public SchematronRule(RuleSet ruleSet, String noticeType) {
}

for (ValidationRule validationRule : ruleSet) {
if (appliesToNoticeType(validationRule, noticeType)) {
if (filter.test(validationRule)) {
if (validationRule instanceof ReportRule) {
tests.add(new SchematronReport(validationRule, this.context));
} else {
Expand All @@ -61,7 +72,7 @@ public SchematronRule(RuleSet ruleSet, String noticeType) {
}

ValidationRule fallback = ruleSet.getFallbackRule();
if (fallback != null && appliesToNoticeType(fallback, noticeType)) {
if (fallback != null && filter.test(fallback)) {
if (fallback instanceof ReportRule) {
tests.add(new SchematronReport(fallback, this.context));
} else {
Expand All @@ -73,9 +84,14 @@ public SchematronRule(RuleSet ruleSet, String noticeType) {
this.tests = tests;
}

private static boolean appliesToNoticeType(ValidationRule rule, String noticeType) {
return rule.getNoticeSubtypes() != null
&& rule.getNoticeSubtypes().asList().contains(noticeType);
private static boolean isUniversal(ValidationRule rule) {
return rule.getNoticeSubtypeRange() != null
&& rule.getNoticeSubtypeRange().isUniversal();
}

private static boolean appliesToNoticeSubtype(ValidationRule rule, String noticeSubtype) {
return rule.getNoticeSubtypeRange() != null
&& rule.getNoticeSubtypeRange().asList().contains(noticeSubtype);
}

/** Used by pattern.ftl */
Expand Down
4 changes: 2 additions & 2 deletions src/main/java/eu/europa/ted/efx/model/rules/AssertRule.java
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,6 @@ public AssertRule(ValidationRule other) {
this.invertedCondition = other.invertedCondition;
this.expression = other.expression;
this.invertedConditionOrExpression = other.invertedConditionOrExpression;
this.noticeSubtypes = other.noticeSubtypes;
this.noticeSubtypeRange = other.noticeSubtypeRange;
}
}
}
Loading