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
61 changes: 61 additions & 0 deletions src/main/java/eu/europa/ted/efx/EfxTranslator.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,12 @@
import java.io.InputStream;
import java.nio.file.Path;
import java.util.Map;
import java.util.Set;

import eu.europa.ted.efx.component.EfxTranslatorFactory;
import eu.europa.ted.efx.interfaces.TranslatorDependencyFactory;
import eu.europa.ted.efx.interfaces.TranslatorOptions;
import eu.europa.ted.efx.model.dependencies.DependencyGraph;

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

//#endregion Translate EFX rules --------------------------------------------

//#region Extract EFX dependencies -------------------------------------------

/**
* Instantiates an EFX compute dependency extractor and extracts all field and node identifiers
* referenced in the given expression.
*
* @param dependencyFactory A {@link TranslatorDependencyFactory} to be used for instantiating the
* dependencies of the extractor.
* @param sdkVersion The version of the eForms SDK that defines the EFX grammar used by the
* expression to be analysed.
* @param expression The EFX expression to analyse.
* @return An unmodifiable set of field and node identifiers referenced in the expression.
* @throws InstantiationException If the dependency extractor cannot be instantiated.
*/
public static Set<String> extractComputeDependencies(final TranslatorDependencyFactory dependencyFactory,
final String sdkVersion, final String expression) throws InstantiationException {
return EfxTranslatorFactory.getEfxComputeDependencyExtractor(sdkVersion, dependencyFactory)
.extractDependencies(expression);
}

/**
* Instantiates an EFX validation dependency extractor and extracts the dependency graph
* from the given EFX rules string.
*
* @param dependencyFactory A {@link TranslatorDependencyFactory} to be used for instantiating the
* dependencies of the extractor.
* @param sdkVersion The version of the eForms SDK.
* @param rules The EFX rules to analyse.
* @return A {@link DependencyGraph} with all dependencies and reverse dependencies.
* @throws InstantiationException If the dependency extractor cannot be instantiated.
*/
public static DependencyGraph extractValidationDependencies(
final TranslatorDependencyFactory dependencyFactory, final String sdkVersion,
final String rules) throws InstantiationException {
return EfxTranslatorFactory.getEfxValidationDependencyExtractor(sdkVersion, dependencyFactory)
.extractDependencyGraph(rules);
}

/**
* Instantiates an EFX validation dependency extractor and extracts the dependency graph
* from the given EFX rules file.
*
* @param dependencyFactory A {@link TranslatorDependencyFactory} to be used for instantiating the
* dependencies of the extractor.
* @param sdkVersion The version of the eForms SDK.
* @param pathname The path to the EFX rules file.
* @return A {@link DependencyGraph} with all dependencies and reverse dependencies.
* @throws IOException If the file cannot be read.
* @throws InstantiationException If the dependency extractor cannot be instantiated.
*/
public static DependencyGraph extractValidationDependencies(
final TranslatorDependencyFactory dependencyFactory, final String sdkVersion,
final Path pathname) throws IOException, InstantiationException {
return EfxTranslatorFactory.getEfxValidationDependencyExtractor(sdkVersion, dependencyFactory)
.extractDependencyGraph(pathname);
}

//#endregion Extract EFX dependencies ----------------------------------------
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
import eu.europa.ted.efx.interfaces.SymbolResolver;
import eu.europa.ted.efx.interfaces.TranslatorDependencyFactory;
import eu.europa.ted.efx.interfaces.TranslatorOptions;
import eu.europa.ted.efx.interfaces.EfxComputeDependencyExtractor;
import eu.europa.ted.efx.interfaces.EfxValidationDependencyExtractor;
import eu.europa.ted.efx.interfaces.ValidatorGenerator;

public class EfxTranslatorFactory extends SdkComponentFactory {
Expand Down Expand Up @@ -71,4 +73,36 @@ public static EfxRulesTranslator getEfxRulesTranslator(final String sdkVersion,
SdkComponentType.EFX_RULES_TRANSLATOR, qualifier, EfxRulesTranslator.class,
validatorGenerator, symbolResolver, scriptGenerator, factory.createErrorListener());
}

public static EfxComputeDependencyExtractor getEfxComputeDependencyExtractor(final String sdkVersion,
final TranslatorDependencyFactory factory) throws InstantiationException {
return getEfxComputeDependencyExtractor(sdkVersion, "", factory);
}

public static EfxComputeDependencyExtractor getEfxComputeDependencyExtractor(final String sdkVersion,
final String qualifier, final TranslatorDependencyFactory factory)
throws InstantiationException {

SymbolResolver symbolResolver = factory.createSymbolResolver(sdkVersion, qualifier);

return EfxTranslatorFactory.INSTANCE.getComponentImpl(sdkVersion,
SdkComponentType.EFX_COMPUTE_DEPENDENCY_EXTRACTOR, qualifier, EfxComputeDependencyExtractor.class,
symbolResolver, factory.createErrorListener());
}

public static EfxValidationDependencyExtractor getEfxValidationDependencyExtractor(final String sdkVersion,
final TranslatorDependencyFactory factory) throws InstantiationException {
return getEfxValidationDependencyExtractor(sdkVersion, "", factory);
}

public static EfxValidationDependencyExtractor getEfxValidationDependencyExtractor(final String sdkVersion,
final String qualifier, final TranslatorDependencyFactory factory)
throws InstantiationException {

SymbolResolver symbolResolver = factory.createSymbolResolver(sdkVersion, qualifier);

return EfxTranslatorFactory.INSTANCE.getComponentImpl(sdkVersion,
SdkComponentType.EFX_VALIDATION_DEPENDENCY_EXTRACTOR, qualifier, EfxValidationDependencyExtractor.class,
symbolResolver, factory.createErrorListener());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package eu.europa.ted.efx.interfaces;

import java.util.Set;

/**
* Defines the API of an EFX compute dependency extractor.
*
* Given an EFX single expression, extracts all field and node identifiers referenced in it.
*/
public interface EfxComputeDependencyExtractor {

/**
* Extracts all field and node identifiers referenced in the given EFX expression.
*
* @param expression A string containing the EFX single expression to analyse.
* @return An unmodifiable set of field and node identifiers referenced in the expression.
*/
Set<String> extractDependencies(final String expression);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package eu.europa.ted.efx.interfaces;

import java.io.IOException;
import java.nio.file.Path;

import eu.europa.ted.efx.model.dependencies.DependencyGraph;

/**
* Defines the API of an EFX validation dependency extractor.
*
* Given an EFX rules file, extracts all field and node dependencies for each validation rule
* and builds a dependency graph.
*/
public interface EfxValidationDependencyExtractor {

/**
* Extracts a dependency graph from the given EFX rules string.
*
* @param rules A string containing EFX validation rules.
* @return A {@link DependencyGraph} mapping each rule's target to its dependencies.
*/
DependencyGraph extractDependencyGraph(final String rules);

/**
* Extracts a dependency graph from an EFX rules file.
*
* @param pathname The path to the EFX rules file.
* @return A {@link DependencyGraph} mapping each rule's target to its dependencies.
* @throws IOException If the file cannot be read.
*/
DependencyGraph extractDependencyGraph(final Path pathname) throws IOException;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
package eu.europa.ted.efx.model.dependencies;

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

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;

/**
* A dependency graph mapping targets (fields and nodes) to their dependencies and dependants.
*
* The graph is built incrementally by the dependency extractor: assert dependencies are added
* per-rule during the tree walk, and reverse dependencies (requiredBy) are computed at the end.
*/
public class DependencyGraph {

private static final ObjectMapper MAPPER = new ObjectMapper();

private final Map<String, TargetDependencies> fieldEntries = new LinkedHashMap<>();
private final Map<String, TargetDependencies> nodeEntries = new LinkedHashMap<>();

public TargetDependencies getOrCreateFieldEntry(final String fieldId) {
return this.fieldEntries.computeIfAbsent(fieldId, id -> new TargetDependencies(id, true));
}

public TargetDependencies getOrCreateNodeEntry(final String nodeId) {
return this.nodeEntries.computeIfAbsent(nodeId, id -> new TargetDependencies(id, false));
}

public List<TargetDependencies> getFieldEntries() {
return new ArrayList<>(this.fieldEntries.values());
}

public List<TargetDependencies> getNodeEntries() {
return new ArrayList<>(this.nodeEntries.values());
}

/**
* Computes the reverse dependencies (requiredBy) from the forward dependencies (dependsOn).
* Must be called after all forward dependencies have been added.
*/
public void computeRequiredBy() {
for (TargetDependencies target : new ArrayList<>(this.fieldEntries.values())) {
this.addRequiredByFromAssertDeps(target);
}
for (TargetDependencies target : new ArrayList<>(this.nodeEntries.values())) {
this.addRequiredByFromAssertDeps(target);
}
}

private void addRequiredByFromAssertDeps(final TargetDependencies target) {
for (RuleDependency rule : target.getAssertDependencies()) {
for (String depFieldId : rule.getFields()) {
this.addRequiredByAssert(this.getOrCreateFieldEntry(depFieldId), target);
}
for (String depNodeId : rule.getNodes()) {
this.addRequiredByAssert(this.getOrCreateNodeEntry(depNodeId), target);
}
}
}

private void addRequiredByAssert(final TargetDependencies dependency,
final TargetDependencies requirer) {
if (requirer.isField()) {
dependency.addRequiredByAssertField(requirer.getId());
} else {
dependency.addRequiredByAssertNode(requirer.getId());
}
}

public String toJson() {
final ObjectNode root = MAPPER.createObjectNode();
root.set("fields", this.serializeEntries(this.fieldEntries));
root.set("nodes", this.serializeEntries(this.nodeEntries));
return root.toPrettyString();
}

private ArrayNode serializeEntries(final Map<String, TargetDependencies> entries) {
final ArrayNode array = MAPPER.createArrayNode();
for (TargetDependencies entry : entries.values()) {
array.add(this.serializeEntry(entry));
}
return array;
}

private ObjectNode serializeEntry(final TargetDependencies entry) {
final ObjectNode node = MAPPER.createObjectNode();
node.put("id", entry.getId());
this.putIfNotEmpty("dependsOn", this.serializeDependsOn(entry), node);
this.putIfNotEmpty("requiredBy", this.serializeRequiredBy(entry), node);
return node;
}

private ObjectNode serializeDependsOn(final TargetDependencies entry) {
final ObjectNode dependsOn = MAPPER.createObjectNode();
this.putIfNotEmpty("compute", this.serializeIdentifierSets(
entry.getComputeFieldDeps(), entry.getComputeNodeDeps()), dependsOn);

final ArrayNode assertArray = MAPPER.createArrayNode();
for (RuleDependency rule : entry.getAssertDependencies()) {
final ObjectNode ruleNode = MAPPER.createObjectNode();
ruleNode.put("ruleId", rule.getRuleId());
this.putIfNotEmpty("fields", this.toStringArray(rule.getFields()), ruleNode);
this.putIfNotEmpty("nodes", this.toStringArray(rule.getNodes()), ruleNode);
this.putIfNotEmpty("codeLists", this.toStringArray(rule.getCodelists()), ruleNode);
assertArray.add(ruleNode);
}
this.putIfNotEmpty("assert", assertArray, dependsOn);
return dependsOn;
}

private ObjectNode serializeRequiredBy(final TargetDependencies entry) {
final ObjectNode requiredBy = MAPPER.createObjectNode();
this.putIfNotEmpty("compute", this.serializeIdentifierSets(
entry.getRequiredByComputeFields(), entry.getRequiredByComputeNodes()), requiredBy);
this.putIfNotEmpty("assert", this.serializeIdentifierSets(
entry.getRequiredByAssertFields(), entry.getRequiredByAssertNodes()), requiredBy);
return requiredBy;
}

private ObjectNode serializeIdentifierSets(final Iterable<String> fields,
final Iterable<String> nodes) {
final ObjectNode obj = MAPPER.createObjectNode();
this.putIfNotEmpty("fields", this.toStringArray(fields), obj);
this.putIfNotEmpty("nodes", this.toStringArray(nodes), obj);
return obj;
}

private void putIfNotEmpty(final String name, final ObjectNode value, final ObjectNode parent) {
if (value.size() > 0) {
parent.set(name, value);
}
}

private void putIfNotEmpty(final String name, final ArrayNode value, final ObjectNode parent) {
if (value.size() > 0) {
parent.set(name, value);
}
}

private ArrayNode toStringArray(final Iterable<String> values) {
final ArrayNode array = MAPPER.createArrayNode();
for (String value : values) {
array.add(value);
}
return array;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package eu.europa.ted.efx.model.dependencies;

import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.Set;

/**
* A set of field and node identifiers collected during a parse tree walk.
* Used as a stack frame in the dependency extraction process.
*/
public class DependencySet {

private final Set<String> fieldIds = new LinkedHashSet<>();
private final Set<String> nodeIds = new LinkedHashSet<>();
private final Set<String> codelistNames = new LinkedHashSet<>();

public void addField(final String fieldId) {
this.fieldIds.add(fieldId);
}

public void addNode(final String nodeId) {
this.nodeIds.add(nodeId);
}

public void addCodelist(final String codelistName) {
this.codelistNames.add(codelistName);
}

public void removeField(final String fieldId) {
this.fieldIds.remove(fieldId);
}

public void removeNode(final String nodeId) {
this.nodeIds.remove(nodeId);
}

public void addAll(final DependencySet other) {
this.fieldIds.addAll(other.fieldIds);
this.nodeIds.addAll(other.nodeIds);
this.codelistNames.addAll(other.codelistNames);
}

public Set<String> getFieldIds() {
return Collections.unmodifiableSet(this.fieldIds);
}

public Set<String> getNodeIds() {
return Collections.unmodifiableSet(this.nodeIds);
}

public Set<String> getCodelistNames() {
return Collections.unmodifiableSet(this.codelistNames);
}

public boolean isEmpty() {
return this.fieldIds.isEmpty() && this.nodeIds.isEmpty() && this.codelistNames.isEmpty();
}

public Set<String> allIds() {
final Set<String> result = new LinkedHashSet<>();
result.addAll(this.fieldIds);
result.addAll(this.nodeIds);
result.addAll(this.codelistNames);
return Collections.unmodifiableSet(result);
}
}
Loading
Loading