Skip to content

Commit 9133127

Browse files
Merge pull request #163 from OP-TED/TEDEFO-5001-dependency-graph
TEDEFO 5001 dependency graph
2 parents 507be8f + 3767763 commit 9133127

36 files changed

Lines changed: 1774 additions & 0 deletions

File tree

src/main/java/eu/europa/ted/efx/EfxTranslator.java

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,12 @@
1717
import java.io.InputStream;
1818
import java.nio.file.Path;
1919
import java.util.Map;
20+
import java.util.Set;
2021

2122
import eu.europa.ted.efx.component.EfxTranslatorFactory;
2223
import eu.europa.ted.efx.interfaces.TranslatorDependencyFactory;
2324
import eu.europa.ted.efx.interfaces.TranslatorOptions;
25+
import eu.europa.ted.efx.model.dependencies.DependencyGraph;
2426

2527
/**
2628
* Provided for convenience, this class exposes static methods that allow you to quickly instantiate
@@ -270,4 +272,63 @@ public static Map<String, String> translateRules(final TranslatorDependencyFacto
270272
}
271273

272274
//#endregion Translate EFX rules --------------------------------------------
275+
276+
//#region Extract EFX dependencies -------------------------------------------
277+
278+
/**
279+
* Instantiates an EFX compute dependency extractor and extracts all field and node identifiers
280+
* referenced in the given expression.
281+
*
282+
* @param dependencyFactory A {@link TranslatorDependencyFactory} to be used for instantiating the
283+
* dependencies of the extractor.
284+
* @param sdkVersion The version of the eForms SDK that defines the EFX grammar used by the
285+
* expression to be analysed.
286+
* @param expression The EFX expression to analyse.
287+
* @return An unmodifiable set of field and node identifiers referenced in the expression.
288+
* @throws InstantiationException If the dependency extractor cannot be instantiated.
289+
*/
290+
public static Set<String> extractComputeDependencies(final TranslatorDependencyFactory dependencyFactory,
291+
final String sdkVersion, final String expression) throws InstantiationException {
292+
return EfxTranslatorFactory.getEfxComputeDependencyExtractor(sdkVersion, dependencyFactory)
293+
.extractDependencies(expression);
294+
}
295+
296+
/**
297+
* Instantiates an EFX validation dependency extractor and extracts the dependency graph
298+
* from the given EFX rules string.
299+
*
300+
* @param dependencyFactory A {@link TranslatorDependencyFactory} to be used for instantiating the
301+
* dependencies of the extractor.
302+
* @param sdkVersion The version of the eForms SDK.
303+
* @param rules The EFX rules to analyse.
304+
* @return A {@link DependencyGraph} with all dependencies and reverse dependencies.
305+
* @throws InstantiationException If the dependency extractor cannot be instantiated.
306+
*/
307+
public static DependencyGraph extractValidationDependencies(
308+
final TranslatorDependencyFactory dependencyFactory, final String sdkVersion,
309+
final String rules) throws InstantiationException {
310+
return EfxTranslatorFactory.getEfxValidationDependencyExtractor(sdkVersion, dependencyFactory)
311+
.extractDependencyGraph(rules);
312+
}
313+
314+
/**
315+
* Instantiates an EFX validation dependency extractor and extracts the dependency graph
316+
* from the given EFX rules file.
317+
*
318+
* @param dependencyFactory A {@link TranslatorDependencyFactory} to be used for instantiating the
319+
* dependencies of the extractor.
320+
* @param sdkVersion The version of the eForms SDK.
321+
* @param pathname The path to the EFX rules file.
322+
* @return A {@link DependencyGraph} with all dependencies and reverse dependencies.
323+
* @throws IOException If the file cannot be read.
324+
* @throws InstantiationException If the dependency extractor cannot be instantiated.
325+
*/
326+
public static DependencyGraph extractValidationDependencies(
327+
final TranslatorDependencyFactory dependencyFactory, final String sdkVersion,
328+
final Path pathname) throws IOException, InstantiationException {
329+
return EfxTranslatorFactory.getEfxValidationDependencyExtractor(sdkVersion, dependencyFactory)
330+
.extractDependencyGraph(pathname);
331+
}
332+
333+
//#endregion Extract EFX dependencies ----------------------------------------
273334
}

src/main/java/eu/europa/ted/efx/component/EfxTranslatorFactory.java

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
import eu.europa.ted.efx.interfaces.SymbolResolver;
1111
import eu.europa.ted.efx.interfaces.TranslatorDependencyFactory;
1212
import eu.europa.ted.efx.interfaces.TranslatorOptions;
13+
import eu.europa.ted.efx.interfaces.EfxComputeDependencyExtractor;
14+
import eu.europa.ted.efx.interfaces.EfxValidationDependencyExtractor;
1315
import eu.europa.ted.efx.interfaces.ValidatorGenerator;
1416

1517
public class EfxTranslatorFactory extends SdkComponentFactory {
@@ -71,4 +73,36 @@ public static EfxRulesTranslator getEfxRulesTranslator(final String sdkVersion,
7173
SdkComponentType.EFX_RULES_TRANSLATOR, qualifier, EfxRulesTranslator.class,
7274
validatorGenerator, symbolResolver, scriptGenerator, factory.createErrorListener());
7375
}
76+
77+
public static EfxComputeDependencyExtractor getEfxComputeDependencyExtractor(final String sdkVersion,
78+
final TranslatorDependencyFactory factory) throws InstantiationException {
79+
return getEfxComputeDependencyExtractor(sdkVersion, "", factory);
80+
}
81+
82+
public static EfxComputeDependencyExtractor getEfxComputeDependencyExtractor(final String sdkVersion,
83+
final String qualifier, final TranslatorDependencyFactory factory)
84+
throws InstantiationException {
85+
86+
SymbolResolver symbolResolver = factory.createSymbolResolver(sdkVersion, qualifier);
87+
88+
return EfxTranslatorFactory.INSTANCE.getComponentImpl(sdkVersion,
89+
SdkComponentType.EFX_COMPUTE_DEPENDENCY_EXTRACTOR, qualifier, EfxComputeDependencyExtractor.class,
90+
symbolResolver, factory.createErrorListener());
91+
}
92+
93+
public static EfxValidationDependencyExtractor getEfxValidationDependencyExtractor(final String sdkVersion,
94+
final TranslatorDependencyFactory factory) throws InstantiationException {
95+
return getEfxValidationDependencyExtractor(sdkVersion, "", factory);
96+
}
97+
98+
public static EfxValidationDependencyExtractor getEfxValidationDependencyExtractor(final String sdkVersion,
99+
final String qualifier, final TranslatorDependencyFactory factory)
100+
throws InstantiationException {
101+
102+
SymbolResolver symbolResolver = factory.createSymbolResolver(sdkVersion, qualifier);
103+
104+
return EfxTranslatorFactory.INSTANCE.getComponentImpl(sdkVersion,
105+
SdkComponentType.EFX_VALIDATION_DEPENDENCY_EXTRACTOR, qualifier, EfxValidationDependencyExtractor.class,
106+
symbolResolver, factory.createErrorListener());
107+
}
74108
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package eu.europa.ted.efx.interfaces;
2+
3+
import java.util.Set;
4+
5+
/**
6+
* Defines the API of an EFX compute dependency extractor.
7+
*
8+
* Given an EFX single expression, extracts all field and node identifiers referenced in it.
9+
*/
10+
public interface EfxComputeDependencyExtractor {
11+
12+
/**
13+
* Extracts all field and node identifiers referenced in the given EFX expression.
14+
*
15+
* @param expression A string containing the EFX single expression to analyse.
16+
* @return An unmodifiable set of field and node identifiers referenced in the expression.
17+
*/
18+
Set<String> extractDependencies(final String expression);
19+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package eu.europa.ted.efx.interfaces;
2+
3+
import java.io.IOException;
4+
import java.nio.file.Path;
5+
6+
import eu.europa.ted.efx.model.dependencies.DependencyGraph;
7+
8+
/**
9+
* Defines the API of an EFX validation dependency extractor.
10+
*
11+
* Given an EFX rules file, extracts all field and node dependencies for each validation rule
12+
* and builds a dependency graph.
13+
*/
14+
public interface EfxValidationDependencyExtractor {
15+
16+
/**
17+
* Extracts a dependency graph from the given EFX rules string.
18+
*
19+
* @param rules A string containing EFX validation rules.
20+
* @return A {@link DependencyGraph} mapping each rule's target to its dependencies.
21+
*/
22+
DependencyGraph extractDependencyGraph(final String rules);
23+
24+
/**
25+
* Extracts a dependency graph from an EFX rules file.
26+
*
27+
* @param pathname The path to the EFX rules file.
28+
* @return A {@link DependencyGraph} mapping each rule's target to its dependencies.
29+
* @throws IOException If the file cannot be read.
30+
*/
31+
DependencyGraph extractDependencyGraph(final Path pathname) throws IOException;
32+
}
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
package eu.europa.ted.efx.model.dependencies;
2+
3+
import java.util.ArrayList;
4+
import java.util.LinkedHashMap;
5+
import java.util.List;
6+
import java.util.Map;
7+
8+
import com.fasterxml.jackson.databind.ObjectMapper;
9+
import com.fasterxml.jackson.databind.node.ArrayNode;
10+
import com.fasterxml.jackson.databind.node.ObjectNode;
11+
12+
/**
13+
* A dependency graph mapping targets (fields and nodes) to their dependencies and dependants.
14+
*
15+
* The graph is built incrementally by the dependency extractor: assert dependencies are added
16+
* per-rule during the tree walk, and reverse dependencies (requiredBy) are computed at the end.
17+
*/
18+
public class DependencyGraph {
19+
20+
private static final ObjectMapper MAPPER = new ObjectMapper();
21+
22+
private final Map<String, TargetDependencies> fieldEntries = new LinkedHashMap<>();
23+
private final Map<String, TargetDependencies> nodeEntries = new LinkedHashMap<>();
24+
25+
public TargetDependencies getOrCreateFieldEntry(final String fieldId) {
26+
return this.fieldEntries.computeIfAbsent(fieldId, id -> new TargetDependencies(id, true));
27+
}
28+
29+
public TargetDependencies getOrCreateNodeEntry(final String nodeId) {
30+
return this.nodeEntries.computeIfAbsent(nodeId, id -> new TargetDependencies(id, false));
31+
}
32+
33+
public List<TargetDependencies> getFieldEntries() {
34+
return new ArrayList<>(this.fieldEntries.values());
35+
}
36+
37+
public List<TargetDependencies> getNodeEntries() {
38+
return new ArrayList<>(this.nodeEntries.values());
39+
}
40+
41+
/**
42+
* Computes the reverse dependencies (requiredBy) from the forward dependencies (dependsOn).
43+
* Must be called after all forward dependencies have been added.
44+
*/
45+
public void computeRequiredBy() {
46+
for (TargetDependencies target : new ArrayList<>(this.fieldEntries.values())) {
47+
this.addRequiredByFromAssertDeps(target);
48+
}
49+
for (TargetDependencies target : new ArrayList<>(this.nodeEntries.values())) {
50+
this.addRequiredByFromAssertDeps(target);
51+
}
52+
}
53+
54+
private void addRequiredByFromAssertDeps(final TargetDependencies target) {
55+
for (RuleDependency rule : target.getAssertDependencies()) {
56+
for (String depFieldId : rule.getFields()) {
57+
this.addRequiredByAssert(this.getOrCreateFieldEntry(depFieldId), target);
58+
}
59+
for (String depNodeId : rule.getNodes()) {
60+
this.addRequiredByAssert(this.getOrCreateNodeEntry(depNodeId), target);
61+
}
62+
}
63+
}
64+
65+
private void addRequiredByAssert(final TargetDependencies dependency,
66+
final TargetDependencies requirer) {
67+
if (requirer.isField()) {
68+
dependency.addRequiredByAssertField(requirer.getId());
69+
} else {
70+
dependency.addRequiredByAssertNode(requirer.getId());
71+
}
72+
}
73+
74+
public String toJson() {
75+
final ObjectNode root = MAPPER.createObjectNode();
76+
root.set("fields", this.serializeEntries(this.fieldEntries));
77+
root.set("nodes", this.serializeEntries(this.nodeEntries));
78+
return root.toPrettyString();
79+
}
80+
81+
private ArrayNode serializeEntries(final Map<String, TargetDependencies> entries) {
82+
final ArrayNode array = MAPPER.createArrayNode();
83+
for (TargetDependencies entry : entries.values()) {
84+
array.add(this.serializeEntry(entry));
85+
}
86+
return array;
87+
}
88+
89+
private ObjectNode serializeEntry(final TargetDependencies entry) {
90+
final ObjectNode node = MAPPER.createObjectNode();
91+
node.put("id", entry.getId());
92+
this.putIfNotEmpty("dependsOn", this.serializeDependsOn(entry), node);
93+
this.putIfNotEmpty("requiredBy", this.serializeRequiredBy(entry), node);
94+
return node;
95+
}
96+
97+
private ObjectNode serializeDependsOn(final TargetDependencies entry) {
98+
final ObjectNode dependsOn = MAPPER.createObjectNode();
99+
this.putIfNotEmpty("compute", this.serializeIdentifierSets(
100+
entry.getComputeFieldDeps(), entry.getComputeNodeDeps()), dependsOn);
101+
102+
final ArrayNode assertArray = MAPPER.createArrayNode();
103+
for (RuleDependency rule : entry.getAssertDependencies()) {
104+
final ObjectNode ruleNode = MAPPER.createObjectNode();
105+
ruleNode.put("ruleId", rule.getRuleId());
106+
this.putIfNotEmpty("fields", this.toStringArray(rule.getFields()), ruleNode);
107+
this.putIfNotEmpty("nodes", this.toStringArray(rule.getNodes()), ruleNode);
108+
this.putIfNotEmpty("codeLists", this.toStringArray(rule.getCodelists()), ruleNode);
109+
assertArray.add(ruleNode);
110+
}
111+
this.putIfNotEmpty("assert", assertArray, dependsOn);
112+
return dependsOn;
113+
}
114+
115+
private ObjectNode serializeRequiredBy(final TargetDependencies entry) {
116+
final ObjectNode requiredBy = MAPPER.createObjectNode();
117+
this.putIfNotEmpty("compute", this.serializeIdentifierSets(
118+
entry.getRequiredByComputeFields(), entry.getRequiredByComputeNodes()), requiredBy);
119+
this.putIfNotEmpty("assert", this.serializeIdentifierSets(
120+
entry.getRequiredByAssertFields(), entry.getRequiredByAssertNodes()), requiredBy);
121+
return requiredBy;
122+
}
123+
124+
private ObjectNode serializeIdentifierSets(final Iterable<String> fields,
125+
final Iterable<String> nodes) {
126+
final ObjectNode obj = MAPPER.createObjectNode();
127+
this.putIfNotEmpty("fields", this.toStringArray(fields), obj);
128+
this.putIfNotEmpty("nodes", this.toStringArray(nodes), obj);
129+
return obj;
130+
}
131+
132+
private void putIfNotEmpty(final String name, final ObjectNode value, final ObjectNode parent) {
133+
if (value.size() > 0) {
134+
parent.set(name, value);
135+
}
136+
}
137+
138+
private void putIfNotEmpty(final String name, final ArrayNode value, final ObjectNode parent) {
139+
if (value.size() > 0) {
140+
parent.set(name, value);
141+
}
142+
}
143+
144+
private ArrayNode toStringArray(final Iterable<String> values) {
145+
final ArrayNode array = MAPPER.createArrayNode();
146+
for (String value : values) {
147+
array.add(value);
148+
}
149+
return array;
150+
}
151+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
package eu.europa.ted.efx.model.dependencies;
2+
3+
import java.util.Collections;
4+
import java.util.LinkedHashSet;
5+
import java.util.Set;
6+
7+
/**
8+
* A set of field and node identifiers collected during a parse tree walk.
9+
* Used as a stack frame in the dependency extraction process.
10+
*/
11+
public class DependencySet {
12+
13+
private final Set<String> fieldIds = new LinkedHashSet<>();
14+
private final Set<String> nodeIds = new LinkedHashSet<>();
15+
private final Set<String> codelistNames = new LinkedHashSet<>();
16+
17+
public void addField(final String fieldId) {
18+
this.fieldIds.add(fieldId);
19+
}
20+
21+
public void addNode(final String nodeId) {
22+
this.nodeIds.add(nodeId);
23+
}
24+
25+
public void addCodelist(final String codelistName) {
26+
this.codelistNames.add(codelistName);
27+
}
28+
29+
public void removeField(final String fieldId) {
30+
this.fieldIds.remove(fieldId);
31+
}
32+
33+
public void removeNode(final String nodeId) {
34+
this.nodeIds.remove(nodeId);
35+
}
36+
37+
public void addAll(final DependencySet other) {
38+
this.fieldIds.addAll(other.fieldIds);
39+
this.nodeIds.addAll(other.nodeIds);
40+
this.codelistNames.addAll(other.codelistNames);
41+
}
42+
43+
public Set<String> getFieldIds() {
44+
return Collections.unmodifiableSet(this.fieldIds);
45+
}
46+
47+
public Set<String> getNodeIds() {
48+
return Collections.unmodifiableSet(this.nodeIds);
49+
}
50+
51+
public Set<String> getCodelistNames() {
52+
return Collections.unmodifiableSet(this.codelistNames);
53+
}
54+
55+
public boolean isEmpty() {
56+
return this.fieldIds.isEmpty() && this.nodeIds.isEmpty() && this.codelistNames.isEmpty();
57+
}
58+
59+
public Set<String> allIds() {
60+
final Set<String> result = new LinkedHashSet<>();
61+
result.addAll(this.fieldIds);
62+
result.addAll(this.nodeIds);
63+
result.addAll(this.codelistNames);
64+
return Collections.unmodifiableSet(result);
65+
}
66+
}

0 commit comments

Comments
 (0)