diff --git a/src/main/java/eu/europa/ted/eforms/sdk/SdkConstants.java b/src/main/java/eu/europa/ted/eforms/sdk/SdkConstants.java index df3d88c..da21331 100644 --- a/src/main/java/eu/europa/ted/eforms/sdk/SdkConstants.java +++ b/src/main/java/eu/europa/ted/eforms/sdk/SdkConstants.java @@ -15,6 +15,13 @@ public class SdkConstants { public static final String FIELDS_DIR_NAME = "fields"; public static final String NODES_JSON_FILE_NAME = "nodes.json"; + + /** + * Key for the array of nodes of fields/fwd/nodes.json in SDK 2, where fields/fields.json uses + * {@link #FIELDS_JSON_XML_STRUCTURE_KEY} (TEDEFO-5228). + */ + public static final String NODES_JSON_NODES_KEY = "nodes"; + public static final String FIELDS_JSON_FILE_NAME = "fields.json"; public static final String DATA_TYPES_JSON_FILE_NAME = "data-types.json"; public static final String DATA_TYPES_JSON_DATA_TYPES_KEY = "dataTypes"; @@ -58,6 +65,46 @@ public class SdkConstants { */ public static final String FWD_DIR_NAME = "fwd"; + /** + * Key of the map of stable identifiers, in fields/fwd/fields.json and fields/fwd/nodes.json of + * SDK 2 (TEDEFO-1877, TEDEFO-5210). A stable identifier names the field or the node that plays a + * particular role, such as "disclosureDate", and stays the same throughout a major version of the + * SDK, whereas the identifier of the field or the node may change from one minor version to the + * next. The stable identifiers are plain strings, as the map is data and will grow + * (TEDEFO-5230). + * + *

+ * The SDK 2 files call the map "specialPurpose". + *

+ */ + public static final String STABLE_IDS_KEY = "specialPurpose"; + + /** + * Stable identifier of the field holding the disclosure group of a withheld field, which SDK 1 + * named per field as privacy.unpublishedFieldId. + */ + public static final String STABLE_ID_DISCLOSURE_GROUP = "disclosureGroup"; + + /** + * Stable identifier of the field holding the justification code of a withheld field, which + * SDK 1 named per field as privacy.reasonCodeFieldId. + */ + public static final String STABLE_ID_DISCLOSURE_JUSTIFICATION_CODE = + "disclosureJustificationCode"; + + /** + * Stable identifier of the field holding the justification description of a withheld field, + * which SDK 1 named per field as privacy.reasonDescriptionFieldId. + */ + public static final String STABLE_ID_DISCLOSURE_JUSTIFICATION_DESCRIPTION = + "disclosureJustificationDescription"; + + /** + * Stable identifier of the field holding the disclosure date of a withheld field, which SDK 1 + * named per field as privacy.publicationDateFieldId. + */ + public static final String STABLE_ID_DISCLOSURE_DATE = "disclosureDate"; + private SdkConstants() {} public enum SdkResource implements PathResource { diff --git a/src/main/java/eu/europa/ted/eforms/sdk/component/SdkComponentType.java b/src/main/java/eu/europa/ted/eforms/sdk/component/SdkComponentType.java index a81fbce..cd6833c 100644 --- a/src/main/java/eu/europa/ted/eforms/sdk/component/SdkComponentType.java +++ b/src/main/java/eu/europa/ted/eforms/sdk/component/SdkComponentType.java @@ -5,7 +5,7 @@ * factory. */ public enum SdkComponentType { - FIELD, NODE, CODELIST, NOTICE_TYPE, EFX_EXPRESSION_TRANSLATOR, EFX_TEMPLATE_TRANSLATOR, EFX_RULES_TRANSLATOR, + FIELD, NODE, CODELIST, NOTICE_TYPE, NODE_REPOSITORY, FIELD_REPOSITORY, EFX_EXPRESSION_TRANSLATOR, EFX_TEMPLATE_TRANSLATOR, EFX_RULES_TRANSLATOR, EFX_COMPUTE_DEPENDENCY_EXTRACTOR, EFX_VALIDATION_DEPENDENCY_EXTRACTOR, SYMBOL_RESOLVER, SCRIPT_GENERATOR, MARKUP_GENERATOR, VALIDATOR_GENERATOR; } diff --git a/src/main/java/eu/europa/ted/eforms/sdk/entity/SdkDataType.java b/src/main/java/eu/europa/ted/eforms/sdk/entity/SdkDataType.java index 4bacdf9..dfbe49d 100644 --- a/src/main/java/eu/europa/ted/eforms/sdk/entity/SdkDataType.java +++ b/src/main/java/eu/europa/ted/eforms/sdk/entity/SdkDataType.java @@ -22,8 +22,9 @@ * Each field in the SDK has a type (e.g., "text", "date", "amount"). This entity captures * type-level metadata such as the privacy masking value, the attribute that the fields of the type * carry (e.g., the currency of an amount) with its data type, and the code list of the data types - * whose values come from one. Currently hardcoded; will be loaded from data-types.json when it is - * added to the SDK. + * whose values come from one. SDK 1 publishes no data types, so they come from the data-types.json + * resource of this library; SDK 2 publishes them in fields/fwd/data-types.json. Both have the same + * format, so a data type does not depend on the SDK version. */ public class SdkDataType { private final String id; diff --git a/src/main/java/eu/europa/ted/eforms/sdk/entity/SdkField.java b/src/main/java/eu/europa/ted/eforms/sdk/entity/SdkField.java index 45b67ae..6833b47 100644 --- a/src/main/java/eu/europa/ted/eforms/sdk/entity/SdkField.java +++ b/src/main/java/eu/europa/ted/eforms/sdk/entity/SdkField.java @@ -17,7 +17,11 @@ public abstract class SdkField implements Comparable { private final String codelistId; private final boolean repeatable; private final String privacyCode; - private final PrivacySettings privacySettings; + // Not final: SDK 2 no longer names the disclosure fields per field, so they are filled in after + // this field is read, from the stable identifiers of the SDK (TEDEFO-5230). + private PrivacySettings privacySettings; + private final String withholdingCondition; + private final String undisclosedFieldSelector; private final List attributes; private final String attributeOf; private final String attributeName; @@ -119,6 +123,8 @@ protected SdkField(final String id, final String type, final String parentNodeId this.repeatable = repeatable; this.privacyCode = null; this.privacySettings = null; + this.withholdingCondition = null; + this.undisclosedFieldSelector = null; this.attributes = Collections.emptyList(); this.attributeOf = null; this.attributeName = null; @@ -132,9 +138,10 @@ protected SdkField(final JsonNode fieldNode) { this.type = fieldNode.get("type").asText(null); this.codelistId = extractCodelistId(fieldNode); this.repeatable = extractRepeatable(fieldNode); - final JsonNode privacyNode = fieldNode.get("privacy"); - this.privacyCode = privacyNode != null ? privacyNode.get("code").asText(null) : null; - this.privacySettings = extractPrivacy(privacyNode); + this.privacyCode = this.extractDisclosureGroupId(fieldNode); + this.privacySettings = this.extractDisclosureControl(fieldNode); + this.withholdingCondition = this.extractWithholdingCondition(fieldNode); + this.undisclosedFieldSelector = this.extractUndisclosedFieldSelector(fieldNode); this.attributes = extractAttributes(fieldNode); this.attributeOf = fieldNode.has("attributeOf") ? fieldNode.get("attributeOf").asText(null) : null; this.attributeName = fieldNode.has("attributeName") ? fieldNode.get("attributeName").asText(null) : null; @@ -180,6 +187,63 @@ protected boolean extractRepeatable(final JsonNode fieldNode) { return valueNode.asBoolean(false); } + /** + * Returns the identifier of the disclosure group of the field, from the code of the privacy block + * of fields/fields.json. SDK 2 gives it as the groupId of the disclosureControl block instead + * (TEDEFO-5229). + * + *

+ * Called while this field is being constructed, so an override must read the given node only, + * and no state of its own. + *

+ */ + protected String extractDisclosureGroupId(final JsonNode fieldNode) { + final JsonNode privacyNode = fieldNode.get("privacy"); + return privacyNode != null ? privacyNode.get("code").asText(null) : null; + } + + /** + * Returns the fields that hold the disclosure data of this field, from the privacy block of + * fields/fields.json. SDK 2 no longer gives them per field: they are the same for every field, + * and are linked to each field in TEDEFO-5230. + * + *

+ * Called while this field is being constructed, so an override must read the given node only, + * and no state of its own. + *

+ */ + protected PrivacySettings extractDisclosureControl(final JsonNode fieldNode) { + return this.extractPrivacy(fieldNode.get("privacy")); + } + + /** + * Returns the EFX condition under which this field is withheld, for the fields that are withheld + * only for some of their instances, or null for every other field. Only SDK 2 gives it, in the + * disclosureControl block (TEDEFO-5229). It is not translated. + * + *

+ * Called while this field is being constructed, so an override must read the given node only, + * and no state of its own. + *

+ */ + protected String extractWithholdingCondition(final JsonNode fieldNode) { + return null; + } + + /** + * Returns the EFX selector locating this field when it is withheld, for the fields that are + * withheld only for some of their instances, or null for every other field. Only SDK 2 gives it, + * in the disclosureControl block (TEDEFO-5229). It is not translated. + * + *

+ * Called while this field is being constructed, so an override must read the given node only, + * and no state of its own. + *

+ */ + protected String extractUndisclosedFieldSelector(final JsonNode fieldNode) { + return null; + } + protected PrivacySettings extractPrivacy(final JsonNode privacyNode) { if (privacyNode == null) { return null; @@ -215,6 +279,15 @@ public String getType() { return this.type; } + /** + * Returns the type as the SDK declares it, without any version-specific mapping. Lets a version + * that must not inherit such a mapping bypass it (see SdkFieldV1's measure/duration mapping, + * which applies to SDK 1 only). + */ + protected final String getDeclaredType() { + return this.type; + } + public String getCodelistId() { return this.codelistId; } @@ -275,6 +348,33 @@ public PrivacySettings getPrivacySettings() { return this.privacySettings; } + /** + * Sets the fields that hold the disclosure data of this field. SDK 1 names them in the privacy + * block of each withheld field, so they are read with the field; SDK 2 names them once, by their + * stable identifiers in fields/fwd/fields.json, so they are filled in afterwards (TEDEFO-5230). + */ + public void setDisclosureControl(final PrivacySettings disclosureControl) { + this.privacySettings = disclosureControl; + } + + /** + * @return the EFX condition ({@code {fieldId} ${conditionEfx}}) under which this field is + * withheld, as the SDK gives it and untranslated, or null when this field is not withheld + * or is withheld for all of its instances. Only SDK 2 gives it (TEDEFO-5229). + */ + public String getWithholdingCondition() { + return this.withholdingCondition; + } + + /** + * @return the EFX selector ({@code {ND-Root} &{/fieldId[conditionEfx]}}) locating this field + * when it is withheld, as the SDK gives it and untranslated, or null when this field is not + * withheld or is withheld for all of its instances. Only SDK 2 gives it (TEDEFO-5229). + */ + public String getUndisclosedFieldSelector() { + return this.undisclosedFieldSelector; + } + public SdkNode getParentNode() { return this.parentNode; } diff --git a/src/main/java/eu/europa/ted/eforms/sdk/entity/v2/SdkFieldV2.java b/src/main/java/eu/europa/ted/eforms/sdk/entity/v2/SdkFieldV2.java index d69ad30..871499f 100644 --- a/src/main/java/eu/europa/ted/eforms/sdk/entity/v2/SdkFieldV2.java +++ b/src/main/java/eu/europa/ted/eforms/sdk/entity/v2/SdkFieldV2.java @@ -8,8 +8,26 @@ import eu.europa.ted.eforms.sdk.component.SdkComponentType; import eu.europa.ted.eforms.sdk.entity.v1.SdkFieldV1; +/** + * A field of SDK 2, read from fields/fwd/fields.json (TEDEFO-5229). + * + *

+ * Compared with fields/fields.json, the properties this entity reads differ as follows: repeatable + * is a plain boolean instead of an object with a value; the privacy block is replaced by + * disclosureControl, whose groupId is the privacy code and which also carries the two EFX + * expressions of the fields that are withheld only for some of their instances; and the four + * fields that held the disclosure data are no longer given per field, so they are not read here + * (TEDEFO-5230 links them to each field instead). + *

+ */ @SdkComponent(versions = {"2"}, componentType = SdkComponentType.FIELD) public class SdkFieldV2 extends SdkFieldV1 { + + /** + * Replaces the privacy block of fields/fields.json (TEDEFO-5128). + */ + private static final String DISCLOSURE_CONTROL = "disclosureControl"; + private final String alias; public SdkFieldV2(String id, String type, String parentNodeId, String xpathAbsolute, @@ -40,4 +58,73 @@ public SdkFieldV2( public String getAlias() { return alias; } + + /** + * fields/fwd/fields.json gives repeatable as a plain boolean, where fields/fields.json gives an + * object with a value. Both are read, because an SDK 2 field is read from either file. + */ + @Override + protected boolean extractRepeatable(final JsonNode fieldNode) { + final JsonNode repeatableNode = fieldNode.get("repeatable"); + if (repeatableNode != null && repeatableNode.isBoolean()) { + return repeatableNode.booleanValue(); + } + return super.extractRepeatable(fieldNode); + } + + /** + * fields/fwd/fields.json gives the identifier of the disclosure group as the groupId of the + * disclosureControl block, where fields/fields.json gives it as the code of the privacy block. + */ + @Override + protected String extractDisclosureGroupId(final JsonNode fieldNode) { + if (disclosureControlOf(fieldNode) == null) { + return super.extractDisclosureGroupId(fieldNode); + } + return textOfDisclosureControl(fieldNode, "groupId"); + } + + /** + * fields/fwd/fields.json no longer gives the fields that hold the disclosure data per field: they + * are the same for every field, and TEDEFO-5230 links them to each field instead. They are still + * read from the privacy block of fields/fields.json. + */ + @Override + protected PrivacySettings extractDisclosureControl(final JsonNode fieldNode) { + if (disclosureControlOf(fieldNode) == null) { + return super.extractDisclosureControl(fieldNode); + } + return null; + } + + @Override + protected String extractWithholdingCondition(final JsonNode fieldNode) { + return textOfDisclosureControl(fieldNode, "withholdingCondition"); + } + + @Override + protected String extractUndisclosedFieldSelector(final JsonNode fieldNode) { + return textOfDisclosureControl(fieldNode, "undisclosedFieldSelector"); + } + + /** + * SDK 2 separates duration from measure, so the mapping of measure onto duration that + * {@link SdkFieldV1} applies for SDK 1 must not apply here. + */ + @Override + public String getType() { + return this.getDeclaredType(); + } + + private static JsonNode disclosureControlOf(final JsonNode fieldNode) { + return fieldNode.get(DISCLOSURE_CONTROL); + } + + private static String textOfDisclosureControl(final JsonNode fieldNode, final String property) { + final JsonNode disclosureControl = disclosureControlOf(fieldNode); + if (disclosureControl == null || !disclosureControl.hasNonNull(property)) { + return null; + } + return disclosureControl.get(property).asText(null); + } } diff --git a/src/main/java/eu/europa/ted/eforms/sdk/repository/MapFromJson.java b/src/main/java/eu/europa/ted/eforms/sdk/repository/MapFromJson.java index 11cdd54..ac6c3cb 100644 --- a/src/main/java/eu/europa/ted/eforms/sdk/repository/MapFromJson.java +++ b/src/main/java/eu/europa/ted/eforms/sdk/repository/MapFromJson.java @@ -5,7 +5,11 @@ import java.nio.file.Files; import java.nio.file.Path; import java.text.MessageFormat; +import java.util.Collections; import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.Map; import org.apache.commons.lang3.builder.EqualsBuilder; import org.apache.commons.lang3.builder.HashCodeBuilder; import org.slf4j.Logger; @@ -14,6 +18,7 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; +import eu.europa.ted.eforms.sdk.SdkConstants; public abstract class MapFromJson extends HashMap { private static final long serialVersionUID = 1L; @@ -85,6 +90,37 @@ protected void populateMap(final JsonNode json, final Object... context) populateMap(json); } + /** + * Reads the map of stable identifiers ({@link SdkConstants#STABLE_IDS_KEY}), which + * fields/fwd/fields.json and fields/fwd/nodes.json of SDK 2 each start with. + * + *

+ * Applications use the stable identifier, such as "disclosureDate", instead of hard-coding the + * identifier of the field or the node. + *

+ * + * @param json the root of a file that may start with a map of stable identifiers + * @return the identifier of each field or node, by its stable identifier, in the order of the + * file, or an empty map for the SDK versions whose files carry no such map + */ + protected static Map stableIdsOf(final JsonNode json) { + final JsonNode stableIds = json.get(SdkConstants.STABLE_IDS_KEY); + if (stableIds == null || !stableIds.isObject()) { + return Collections.emptyMap(); + } + + // Keep the order of the file, so that what is read back is what the SDK gives. + final Map idByStableId = new LinkedHashMap<>(); + final Iterator names = stableIds.fieldNames(); + while (names.hasNext()) { + final String stableId = names.next(); + if (stableIds.hasNonNull(stableId)) { + idByStableId.put(stableId, stableIds.get(stableId).asText(null)); + } + } + return Collections.unmodifiableMap(idByStableId); + } + /** * @return A reusable Jackson object mapper instance. */ diff --git a/src/main/java/eu/europa/ted/eforms/sdk/repository/SdkDataTypeRepository.java b/src/main/java/eu/europa/ted/eforms/sdk/repository/SdkDataTypeRepository.java index 31de339..1141450 100644 --- a/src/main/java/eu/europa/ted/eforms/sdk/repository/SdkDataTypeRepository.java +++ b/src/main/java/eu/europa/ted/eforms/sdk/repository/SdkDataTypeRepository.java @@ -16,6 +16,8 @@ import java.io.IOException; import java.io.InputStream; import java.io.UncheckedIOException; +import java.nio.file.Files; +import java.nio.file.Path; import java.util.HashMap; import java.util.Map; import com.fasterxml.jackson.databind.JsonNode; @@ -26,22 +28,34 @@ /** * Repository of SDK data types. * - * Currently uses hardcoded type definitions, in the data-types.json resource of this library. + *

+ * SDK 2 publishes its data types in fields/fwd/data-types.json (TEDEFO-5231). SDK 1 publishes + * none, so its data types come from the data-types.json resource of this library, which has the + * same format. The data types do not depend on the SDK version. + *

*/ public class SdkDataTypeRepository extends HashMap { private static final long serialVersionUID = 1L; /** - * Creates the repository of the data types, with their privacy masks, the attributes that their - * fields carry, and the code lists of their values. They are read from the data-types.json - * resource of this library, which has the format of fields/fwd/data-types.json in SDK 2. The - * data types are the same for every SDK version. + * Creates the repository of the data types of the SDK in the given folder, with their privacy + * masks, the attributes that their fields carry, and the code lists of their values. They are + * read from the fields/fwd/data-types.json of the SDK, or, for an SDK that publishes no data + * types, from the data-types.json resource of this library. + * + * @param sdkPath the folder of the SDK + */ + public SdkDataTypeRepository(final Path sdkPath) { + final Path dataTypesJson = + sdkPath.resolve(SdkConstants.SdkResource.FIELDS_FWD_DATA_TYPES.getPath()); + this.addDataTypes(Files.exists(dataTypesJson) ? readSdkFile(dataTypesJson) : readResource()); + } + + /** + * Creates the repository of the data types from the data-types.json resource of this library. */ public SdkDataTypeRepository() { - for (final JsonNode dataType : readResource().get(SdkConstants.DATA_TYPES_JSON_DATA_TYPES_KEY)) { - final SdkDataType sdkDataType = new SdkDataType(dataType); - this.put(sdkDataType.getId(), sdkDataType); - } + this.addDataTypes(readResource()); } private SdkDataTypeRepository(final Map dataTypes) { @@ -86,6 +100,13 @@ private void addType(String id, String privacyMask) { this.put(id, new SdkDataType(id, privacyMask)); } + private void addDataTypes(final JsonNode json) { + for (final JsonNode dataType : json.get(SdkConstants.DATA_TYPES_JSON_DATA_TYPES_KEY)) { + final SdkDataType sdkDataType = new SdkDataType(dataType); + this.put(sdkDataType.getId(), sdkDataType); + } + } + private static JsonNode readResource() { try (InputStream input = SdkDataTypeRepository.class.getResourceAsStream(SdkConstants.DATA_TYPES_JSON_FILE_NAME)) { @@ -94,4 +115,12 @@ private static JsonNode readResource() { throw new UncheckedIOException(e); } } + + private static JsonNode readSdkFile(final Path dataTypesPath) { + try (InputStream input = Files.newInputStream(dataTypesPath)) { + return new ObjectMapper().readTree(input); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } } diff --git a/src/main/java/eu/europa/ted/eforms/sdk/repository/SdkFieldRepository.java b/src/main/java/eu/europa/ted/eforms/sdk/repository/SdkFieldRepository.java index a06f6aa..98a4855 100644 --- a/src/main/java/eu/europa/ted/eforms/sdk/repository/SdkFieldRepository.java +++ b/src/main/java/eu/europa/ted/eforms/sdk/repository/SdkFieldRepository.java @@ -2,16 +2,39 @@ import java.nio.file.Path; import java.util.ArrayList; +import java.util.Collections; import java.util.List; +import java.util.Map; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ArrayNode; import eu.europa.ted.eforms.sdk.SdkConstants; import eu.europa.ted.eforms.sdk.entity.SdkEntityFactory; import eu.europa.ted.eforms.sdk.entity.SdkField; +/** + * The fields of an SDK, by identifier. + * + *

+ * Each SDK version keeps its fields in its own file. The repository of an SDK version, which knows + * that file, is given by + * {@link SdkRepositoryFactory#getSdkFieldRepository(String, Path, SdkNodeRepository)}. + *

+ */ public class SdkFieldRepository extends MapFromJson { private static final long serialVersionUID = 1L; + /** + * The identifier of each field that plays a particular role, by its stable identifier + * (TEDEFO-5230). Null for the SDK versions that publish no such map. + * + *

+ * Deliberately left without an initialiser: it is assigned while the map of fields is being + * populated, which happens during the call to super, and a field initialiser would run after + * that call and undo it. + *

+ */ + private Map stableIds; + public SdkFieldRepository(String sdkVersion, Path jsonPath) throws InstantiationException { super(sdkVersion, jsonPath); } @@ -23,33 +46,49 @@ public SdkFieldRepository(String sdkVersion, Path jsonPath, SdkNodeRepository no @Override protected void populateMap(final JsonNode json) throws InstantiationException { - populateMap(json, new Object[0]); + this.populateMap(json, new Object[0]); } @Override protected void populateMap(final JsonNode json, final Object... context) throws InstantiationException { - SdkNodeRepository nodes = (context.length > 0 && context[0] instanceof SdkNodeRepository) + this.addFields((ArrayNode) json.get(SdkConstants.FIELDS_JSON_FIELDS_KEY), + nodeRepositoryOf(context)); + this.resolveReferences(); + } + + /** + * @return the repository of nodes given as context, to link each field to its parent node, or + * null to leave the parent nodes unlinked + */ + protected static SdkNodeRepository nodeRepositoryOf(final Object... context) { + return context.length > 0 && context[0] instanceof SdkNodeRepository ? (SdkNodeRepository) context[0] : null; + } - final ArrayNode fields = (ArrayNode) json.get(SdkConstants.FIELDS_JSON_FIELDS_KEY); - - // First pass: create all field entities and add them to the map + /** + * Adds the given fields, each linked to its parent node when the nodes are given. + */ + protected void addFields(final ArrayNode fields, final SdkNodeRepository nodes) + throws InstantiationException { for (final JsonNode field : fields) { - final SdkField sdkField = SdkEntityFactory.getSdkField(sdkVersion, field); - put(sdkField.getId(), sdkField); - + final SdkField sdkField = SdkEntityFactory.getSdkField(this.sdkVersion, field); + this.put(sdkField.getId(), sdkField); if (nodes != null && sdkField.getParentNodeId() != null) { sdkField.setParentNode(nodes.get(sdkField.getParentNodeId())); } } + } - // Second pass: resolve cross-field references + /** + * Resolves the fields that each field refers to by identifier: its disclosure fields, its + * attribute fields and the field it is an attribute of. + */ + protected void resolveReferences() { for (final SdkField sdkField : this.values()) { if (sdkField.getPrivacySettings() != null) { - SdkField.PrivacySettings privacy = sdkField.getPrivacySettings(); - + final SdkField.PrivacySettings privacy = sdkField.getPrivacySettings(); if (privacy.getPrivacyCodeFieldId() != null) { privacy.setPrivacyCodeField(this.get(privacy.getPrivacyCodeFieldId())); } @@ -66,9 +105,9 @@ protected void populateMap(final JsonNode json, final Object... context) } if (!sdkField.getAttributes().isEmpty()) { - List attrFields = new ArrayList<>(); - for (String attrFieldId : sdkField.getAttributes()) { - SdkField attrField = this.get(attrFieldId); + final List attrFields = new ArrayList<>(); + for (final String attrFieldId : sdkField.getAttributes()) { + final SdkField attrField = this.get(attrFieldId); if (attrField != null) { attrFields.add(attrField); } @@ -81,4 +120,28 @@ protected void populateMap(final JsonNode json, final Object... context) } } } + + /** + * Reads the map of stable identifiers, which the given file starts with. + */ + protected void readStableIds(final JsonNode json) { + this.stableIds = stableIdsOf(json); + } + + /** + * @return the identifier of each field that plays a particular role, by its stable identifier, as + * the SDK gives them. Empty for the SDK versions that publish no such map. + */ + public Map getStableIds() { + return this.stableIds == null ? Collections.emptyMap() : this.stableIds; + } + + /** + * @param stableId a stable identifier, such as "disclosureDate" + * @return the identifier of the field that plays that role, or null if the SDK names none. An + * unknown stable identifier is not an error. + */ + public String getFieldIdByStableId(final String stableId) { + return this.getStableIds().get(stableId); + } } diff --git a/src/main/java/eu/europa/ted/eforms/sdk/repository/SdkNodeRepository.java b/src/main/java/eu/europa/ted/eforms/sdk/repository/SdkNodeRepository.java index c853153..1870bf6 100644 --- a/src/main/java/eu/europa/ted/eforms/sdk/repository/SdkNodeRepository.java +++ b/src/main/java/eu/europa/ted/eforms/sdk/repository/SdkNodeRepository.java @@ -2,32 +2,63 @@ import java.nio.file.Path; import java.util.ArrayList; +import java.util.Collections; import java.util.List; +import java.util.Map; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ArrayNode; import eu.europa.ted.eforms.sdk.SdkConstants; import eu.europa.ted.eforms.sdk.entity.SdkEntityFactory; import eu.europa.ted.eforms.sdk.entity.SdkNode; +/** + * The nodes of an SDK, by identifier. + * + *

+ * Each SDK version keeps its nodes in its own file. The repository of an SDK version, which knows + * that file, is given by {@link SdkRepositoryFactory#getSdkNodeRepository(String, Path)}. + *

+ */ public class SdkNodeRepository extends MapFromJson { private static final long serialVersionUID = 1L; + /** + * The identifier of each node that plays a particular role, by its stable identifier + * (TEDEFO-5230). Null for the SDK versions that publish no such map. + * + *

+ * Deliberately left without an initialiser: it is assigned while the map of nodes is being + * populated, which happens during the call to super, and a field initialiser would run after + * that call and undo it. + *

+ */ + private Map stableIds; + + /** + * Creates the repository of the nodes from the xmlStructure array of the given fields.json. + */ public SdkNodeRepository(String sdkVersion, Path jsonPath) throws InstantiationException { super(sdkVersion, jsonPath); } @Override protected void populateMap(final JsonNode json) throws InstantiationException { - final ArrayNode nodes = (ArrayNode) json.get(SdkConstants.FIELDS_JSON_XML_STRUCTURE_KEY); - List needsParentWiring = new ArrayList<>(); + this.addNodes((ArrayNode) json.get(SdkConstants.FIELDS_JSON_XML_STRUCTURE_KEY)); + } + + /** + * Adds the given nodes, each linked to its parent node. + */ + protected void addNodes(final ArrayNode nodes) throws InstantiationException { + final List needsParentWiring = new ArrayList<>(); // First pass: create all nodes, optimistically set parent if already loaded for (final JsonNode node : nodes) { - final SdkNode sdkNode = SdkEntityFactory.getSdkNode(sdkVersion, node); - put(sdkNode.getId(), sdkNode); + final SdkNode sdkNode = SdkEntityFactory.getSdkNode(this.sdkVersion, node); + this.put(sdkNode.getId(), sdkNode); if (sdkNode.getParentId() != null) { - SdkNode parent = get(sdkNode.getParentId()); + final SdkNode parent = this.get(sdkNode.getParentId()); if (parent != null) { sdkNode.setParent(parent); } else { @@ -37,8 +68,32 @@ protected void populateMap(final JsonNode json) throws InstantiationException { } // Second pass: wire up any nodes whose parent wasn't loaded yet - for (SdkNode sdkNode : needsParentWiring) { - sdkNode.setParent(get(sdkNode.getParentId())); + for (final SdkNode sdkNode : needsParentWiring) { + sdkNode.setParent(this.get(sdkNode.getParentId())); } } + + /** + * Reads the map of stable identifiers, which the given file starts with. + */ + protected void readStableIds(final JsonNode json) { + this.stableIds = stableIdsOf(json); + } + + /** + * @return the identifier of each node that plays a particular role, by its stable identifier, as + * the SDK gives them. Empty for the SDK versions that publish no such map. + */ + public Map getStableIds() { + return this.stableIds == null ? Collections.emptyMap() : this.stableIds; + } + + /** + * @param stableId a stable identifier, such as "root" + * @return the identifier of the node that plays that role, or null if the SDK names none. An + * unknown stable identifier is not an error. + */ + public String getNodeIdByStableId(final String stableId) { + return this.getStableIds().get(stableId); + } } diff --git a/src/main/java/eu/europa/ted/eforms/sdk/repository/SdkRepositoryFactory.java b/src/main/java/eu/europa/ted/eforms/sdk/repository/SdkRepositoryFactory.java new file mode 100644 index 0000000..b841d1c --- /dev/null +++ b/src/main/java/eu/europa/ted/eforms/sdk/repository/SdkRepositoryFactory.java @@ -0,0 +1,58 @@ +/* + * Copyright 2026 European Union + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by the European + * Commission – subsequent versions of the EUPL (the "Licence"); You may not use this work except in + * compliance with the Licence. You may obtain a copy of the Licence at: + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software distributed under the Licence + * is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the Licence for the specific language governing permissions and limitations under + * the Licence. + */ +package eu.europa.ted.eforms.sdk.repository; + +import java.nio.file.Path; +import eu.europa.ted.eforms.sdk.component.SdkComponent; +import eu.europa.ted.eforms.sdk.component.SdkComponentFactory; +import eu.europa.ted.eforms.sdk.component.SdkComponentType; + +/** + * Gives the repositories of an SDK version. Each SDK version keeps its metadata in its own files, + * so each has its own repositories, registered with {@link SdkComponent} for its major version. + */ +public class SdkRepositoryFactory extends SdkComponentFactory { + public static final SdkRepositoryFactory INSTANCE = new SdkRepositoryFactory(); + + private SdkRepositoryFactory() { + super(); + } + + /** + * @param sdkVersion the version of the SDK + * @param sdkPath the folder of the SDK + * @return the nodes of that SDK, by identifier + * @throws InstantiationException if the nodes cannot be read for this SDK version + */ + public static SdkNodeRepository getSdkNodeRepository(final String sdkVersion, + final Path sdkPath) throws InstantiationException { + return SdkRepositoryFactory.INSTANCE.getComponentImpl(sdkVersion, + SdkComponentType.NODE_REPOSITORY, SdkNodeRepository.class, sdkVersion, sdkPath); + } + + /** + * @param sdkVersion the version of the SDK + * @param sdkPath the folder of the SDK + * @param nodeRepository the nodes of the same SDK, to link each field to its parent node; may be + * null to leave the parent nodes unlinked + * @return the fields of that SDK, by identifier + * @throws InstantiationException if the fields cannot be read for this SDK version + */ + public static SdkFieldRepository getSdkFieldRepository(final String sdkVersion, + final Path sdkPath, final SdkNodeRepository nodeRepository) throws InstantiationException { + return SdkRepositoryFactory.INSTANCE.getComponentImpl(sdkVersion, + SdkComponentType.FIELD_REPOSITORY, SdkFieldRepository.class, sdkVersion, sdkPath, + nodeRepository); + } +} diff --git a/src/main/java/eu/europa/ted/eforms/sdk/repository/v1/SdkFieldRepositoryV1.java b/src/main/java/eu/europa/ted/eforms/sdk/repository/v1/SdkFieldRepositoryV1.java new file mode 100644 index 0000000..4f8c924 --- /dev/null +++ b/src/main/java/eu/europa/ted/eforms/sdk/repository/v1/SdkFieldRepositoryV1.java @@ -0,0 +1,41 @@ +/* + * Copyright 2026 European Union + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by the European + * Commission – subsequent versions of the EUPL (the "Licence"); You may not use this work except in + * compliance with the Licence. You may obtain a copy of the Licence at: + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software distributed under the Licence + * is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the Licence for the specific language governing permissions and limitations under + * the Licence. + */ +package eu.europa.ted.eforms.sdk.repository.v1; + +import java.nio.file.Path; +import eu.europa.ted.eforms.sdk.SdkConstants; +import eu.europa.ted.eforms.sdk.component.SdkComponent; +import eu.europa.ted.eforms.sdk.component.SdkComponentType; +import eu.europa.ted.eforms.sdk.repository.SdkFieldRepository; +import eu.europa.ted.eforms.sdk.repository.SdkNodeRepository; + +/** + * The fields of an SDK 1, read from its fields/fields.json. + */ +@SdkComponent(versions = {"1"}, componentType = SdkComponentType.FIELD_REPOSITORY) +public class SdkFieldRepositoryV1 extends SdkFieldRepository { + private static final long serialVersionUID = 1L; + + /** + * @param sdkVersion the version of the SDK + * @param sdkPath the folder of the SDK + * @param nodeRepository the nodes of the same SDK, to link each field to its parent node; may be + * null to leave the parent nodes unlinked + */ + public SdkFieldRepositoryV1(final String sdkVersion, final Path sdkPath, + final SdkNodeRepository nodeRepository) throws InstantiationException { + super(sdkVersion, sdkPath.resolve(SdkConstants.SdkResource.FIELDS_JSON.getPath()), + nodeRepository); + } +} diff --git a/src/main/java/eu/europa/ted/eforms/sdk/repository/v1/SdkNodeRepositoryV1.java b/src/main/java/eu/europa/ted/eforms/sdk/repository/v1/SdkNodeRepositoryV1.java new file mode 100644 index 0000000..6329503 --- /dev/null +++ b/src/main/java/eu/europa/ted/eforms/sdk/repository/v1/SdkNodeRepositoryV1.java @@ -0,0 +1,37 @@ +/* + * Copyright 2026 European Union + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by the European + * Commission – subsequent versions of the EUPL (the "Licence"); You may not use this work except in + * compliance with the Licence. You may obtain a copy of the Licence at: + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software distributed under the Licence + * is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the Licence for the specific language governing permissions and limitations under + * the Licence. + */ +package eu.europa.ted.eforms.sdk.repository.v1; + +import java.nio.file.Path; +import eu.europa.ted.eforms.sdk.SdkConstants; +import eu.europa.ted.eforms.sdk.component.SdkComponent; +import eu.europa.ted.eforms.sdk.component.SdkComponentType; +import eu.europa.ted.eforms.sdk.repository.SdkNodeRepository; + +/** + * The nodes of an SDK 1, read from the xmlStructure array of its fields/fields.json. + */ +@SdkComponent(versions = {"1"}, componentType = SdkComponentType.NODE_REPOSITORY) +public class SdkNodeRepositoryV1 extends SdkNodeRepository { + private static final long serialVersionUID = 1L; + + /** + * @param sdkVersion the version of the SDK + * @param sdkPath the folder of the SDK + */ + public SdkNodeRepositoryV1(final String sdkVersion, final Path sdkPath) + throws InstantiationException { + super(sdkVersion, sdkPath.resolve(SdkConstants.SdkResource.FIELDS_JSON.getPath())); + } +} diff --git a/src/main/java/eu/europa/ted/eforms/sdk/repository/v2/SdkFieldRepositoryV2.java b/src/main/java/eu/europa/ted/eforms/sdk/repository/v2/SdkFieldRepositoryV2.java new file mode 100644 index 0000000..4efadf8 --- /dev/null +++ b/src/main/java/eu/europa/ted/eforms/sdk/repository/v2/SdkFieldRepositoryV2.java @@ -0,0 +1,83 @@ +/* + * Copyright 2026 European Union + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by the European + * Commission – subsequent versions of the EUPL (the "Licence"); You may not use this work except in + * compliance with the Licence. You may obtain a copy of the Licence at: + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software distributed under the Licence + * is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the Licence for the specific language governing permissions and limitations under + * the Licence. + */ +package eu.europa.ted.eforms.sdk.repository.v2; + +import java.nio.file.Path; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ArrayNode; +import eu.europa.ted.eforms.sdk.SdkConstants; +import eu.europa.ted.eforms.sdk.component.SdkComponent; +import eu.europa.ted.eforms.sdk.component.SdkComponentType; +import eu.europa.ted.eforms.sdk.entity.SdkField; +import eu.europa.ted.eforms.sdk.repository.SdkFieldRepository; +import eu.europa.ted.eforms.sdk.repository.SdkNodeRepository; + +/** + * The fields of an SDK 2, read from its fields/fwd/fields.json (TEDEFO-5229), with the map of + * stable identifiers, which the file starts with (TEDEFO-5230). + * + *

+ * SDK 2 names the four disclosure fields once, by their stable identifiers, instead of naming them + * in the privacy block of each field that can be withheld. This repository gives them to each such + * field, so that its privacy settings are the same as with SDK 1. + *

+ */ +@SdkComponent(versions = {"2"}, componentType = SdkComponentType.FIELD_REPOSITORY) +public class SdkFieldRepositoryV2 extends SdkFieldRepository { + private static final long serialVersionUID = 1L; + + /** + * @param sdkVersion the version of the SDK + * @param sdkPath the folder of the SDK + * @param nodeRepository the nodes of the same SDK, to link each field to its parent node; may be + * null to leave the parent nodes unlinked + */ + public SdkFieldRepositoryV2(final String sdkVersion, final Path sdkPath, + final SdkNodeRepository nodeRepository) throws InstantiationException { + super(sdkVersion, sdkPath.resolve(SdkConstants.SdkResource.FIELDS_FWD_FIELDS_JSON.getPath()), + nodeRepository); + } + + @Override + protected void populateMap(final JsonNode json, final Object... context) + throws InstantiationException { + this.readStableIds(json); + this.addFields((ArrayNode) json.get(SdkConstants.FIELDS_JSON_FIELDS_KEY), + nodeRepositoryOf(context)); + this.linkTheDisclosureFields(); + this.resolveReferences(); + } + + /** + * Gives the four disclosure fields, found by their stable identifiers, to each field that can be + * withheld, that is each field that has a disclosure control, and therefore a privacy code. They + * are read from the map of stable identifiers only, with no fallback to the privacy blocks of + * fields/fields.json, so a file without that map gives no disclosure fields. + */ + private void linkTheDisclosureFields() { + if (this.getStableIds().isEmpty()) { + return; + } + for (final SdkField sdkField : this.values()) { + if (sdkField.getPrivacyCode() != null) { + sdkField.setDisclosureControl(new SdkField.PrivacySettings( + this.getFieldIdByStableId(SdkConstants.STABLE_ID_DISCLOSURE_GROUP), + this.getFieldIdByStableId(SdkConstants.STABLE_ID_DISCLOSURE_JUSTIFICATION_CODE), + this.getFieldIdByStableId( + SdkConstants.STABLE_ID_DISCLOSURE_JUSTIFICATION_DESCRIPTION), + this.getFieldIdByStableId(SdkConstants.STABLE_ID_DISCLOSURE_DATE))); + } + } + } +} diff --git a/src/main/java/eu/europa/ted/eforms/sdk/repository/v2/SdkNodeRepositoryV2.java b/src/main/java/eu/europa/ted/eforms/sdk/repository/v2/SdkNodeRepositoryV2.java new file mode 100644 index 0000000..82a30c7 --- /dev/null +++ b/src/main/java/eu/europa/ted/eforms/sdk/repository/v2/SdkNodeRepositoryV2.java @@ -0,0 +1,46 @@ +/* + * Copyright 2026 European Union + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by the European + * Commission – subsequent versions of the EUPL (the "Licence"); You may not use this work except in + * compliance with the Licence. You may obtain a copy of the Licence at: + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software distributed under the Licence + * is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the Licence for the specific language governing permissions and limitations under + * the Licence. + */ +package eu.europa.ted.eforms.sdk.repository.v2; + +import java.nio.file.Path; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ArrayNode; +import eu.europa.ted.eforms.sdk.SdkConstants; +import eu.europa.ted.eforms.sdk.component.SdkComponent; +import eu.europa.ted.eforms.sdk.component.SdkComponentType; +import eu.europa.ted.eforms.sdk.repository.SdkNodeRepository; + +/** + * The nodes of an SDK 2, read from its fields/fwd/nodes.json (TEDEFO-5228), with the map of stable + * identifiers, which the file starts with (TEDEFO-5230). + */ +@SdkComponent(versions = {"2"}, componentType = SdkComponentType.NODE_REPOSITORY) +public class SdkNodeRepositoryV2 extends SdkNodeRepository { + private static final long serialVersionUID = 1L; + + /** + * @param sdkVersion the version of the SDK + * @param sdkPath the folder of the SDK + */ + public SdkNodeRepositoryV2(final String sdkVersion, final Path sdkPath) + throws InstantiationException { + super(sdkVersion, sdkPath.resolve(SdkConstants.SdkResource.FIELDS_FWD_NODES_JSON.getPath())); + } + + @Override + protected void populateMap(final JsonNode json) throws InstantiationException { + this.readStableIds(json); + this.addNodes((ArrayNode) json.get(SdkConstants.NODES_JSON_NODES_KEY)); + } +} diff --git a/src/test/java/eu/europa/ted/eforms/sdk/repository/SdkDataTypeRepositoryTest.java b/src/test/java/eu/europa/ted/eforms/sdk/repository/SdkDataTypeRepositoryTest.java index 8c66b92..d1faaba 100644 --- a/src/test/java/eu/europa/ted/eforms/sdk/repository/SdkDataTypeRepositoryTest.java +++ b/src/test/java/eu/europa/ted/eforms/sdk/repository/SdkDataTypeRepositoryTest.java @@ -15,20 +15,42 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import java.nio.file.Path; import java.util.Set; import org.junit.jupiter.api.Test; import eu.europa.ted.eforms.sdk.entity.SdkDataType; class SdkDataTypeRepositoryTest { + /** SDK 1 publishes no data types: they come from the resource of this library. */ + private static final String SDK_1 = "1.16"; + + /** SDK 2 publishes its data types in fields/fwd/data-types.json. */ + private static final String SDK_2 = "2.0"; + + /** + * The folders of the SDKs, one per version. Only the one of SDK 2 holds a + * fields/fwd/data-types.json, deliberately different from the resource. + */ + private static final Path SDK_ROOT = Path.of("src", "test", "resources", "sdk-fixture"); + + private static SdkDataTypeRepository ofSdk1() throws InstantiationException { + return new SdkDataTypeRepository(SDK_ROOT.resolve(SDK_1)); + } + + private static SdkDataTypeRepository ofSdk2() throws InstantiationException { + return new SdkDataTypeRepository(SDK_ROOT.resolve(SDK_2)); + } + @Test - void testAllDataTypesAreLoaded() { - assertEquals(23, new SdkDataTypeRepository().size()); + void testAllDataTypesAreLoaded() throws InstantiationException { + assertEquals(23, ofSdk1().size()); } @Test - void testAmountHasCurrencyAttribute() { - SdkDataType amount = new SdkDataTypeRepository().get("amount"); + void testAmountHasCurrencyAttribute() throws InstantiationException { + SdkDataType amount = ofSdk1().get("amount"); assertEquals("-1", amount.getPrivacyMask()); assertEquals("currencyID", amount.getAttributeName()); @@ -36,24 +58,24 @@ void testAmountHasCurrencyAttribute() { } @Test - void testDurationAndMeasureHaveTheirOwnUnits() { - SdkDataTypeRepository repository = new SdkDataTypeRepository(); + void testDurationAndMeasureHaveTheirOwnUnits() throws InstantiationException { + SdkDataTypeRepository repository = ofSdk1(); assertEquals("duration-unit", repository.get("duration").getAttributeType()); assertEquals("measurement-unit", repository.get("measure").getAttributeType()); } @Test - void testCodelistOfTheUnitsOfDuration() { - SdkDataTypeRepository repository = new SdkDataTypeRepository(); + void testCodelistOfTheUnitsOfDuration() throws InstantiationException { + SdkDataTypeRepository repository = ofSdk1(); assertEquals("timeperiod", repository.get(repository.get("duration").getAttributeType()).getListName()); } @Test - void testTypesWithRootCodelist() { - SdkDataTypeRepository repository = new SdkDataTypeRepository(); + void testTypesWithRootCodelist() throws InstantiationException { + SdkDataTypeRepository repository = ofSdk1(); assertEquals("currency", repository.get("currency").getListName()); assertEquals("timeperiod", repository.get("duration-unit").getListName()); @@ -63,8 +85,8 @@ void testTypesWithRootCodelist() { } @Test - void testAttributeOfTypeText() { - SdkDataTypeRepository repository = new SdkDataTypeRepository(); + void testAttributeOfTypeText() throws InstantiationException { + SdkDataTypeRepository repository = ofSdk1(); assertEquals("listName", repository.get("code").getAttributeName()); assertEquals("text", repository.get("code").getAttributeType()); @@ -74,8 +96,47 @@ void testAttributeOfTypeText() { } @Test - void testTypeWithoutAttribute() { - SdkDataType date = new SdkDataTypeRepository().get("date"); + void testTypeWithoutAttribute() throws InstantiationException { + SdkDataType date = ofSdk1().get("date"); + + assertEquals("1970-01-01Z", date.getPrivacyMask()); + assertNull(date.getAttributeName()); + assertNull(date.getAttributeType()); + assertNull(date.getListName()); + } + + /** + * The point of TEDEFO-5231: with SDK 2 the data types come from the SDK, not from the copy kept + * in this library. The masking value of the fixture differs from the one of the resource, so + * only the file can be the source. + */ + @Test + void testSdk2ReadsTheDataTypesFromTheSdk() throws InstantiationException { + SdkDataTypeRepository repository = ofSdk2(); + + assertEquals(5, repository.size(), "the SDK fixture holds fewer data types than the resource"); + assertEquals("undisclosed", repository.get("amount").getPrivacyMask()); + assertEquals("-1", ofSdk1().get("amount").getPrivacyMask(), + "the resource of SDK 1 must be left alone"); + } + + @Test + void testSdk2ReadsTheAttributesAndTheCodelistsFromTheSdk() throws InstantiationException { + SdkDataTypeRepository repository = ofSdk2(); + + assertEquals("unitCode", repository.get("duration").getAttributeName()); + assertEquals("duration-unit", repository.get("duration").getAttributeType()); + assertEquals("timeperiod", repository.get("duration-unit").getListName()); + assertEquals("currency", repository.get("currency").getListName()); + } + + /** + * The file of the SDK also carries properties that a data type does not read (description, + * valueType, displayTypes). They must be ignored, not break the loading. + */ + @Test + void testSdk2IgnoresThePropertiesThatADataTypeDoesNotRead() throws InstantiationException { + SdkDataType date = ofSdk2().get("date"); assertEquals("1970-01-01Z", date.getPrivacyMask()); assertNull(date.getAttributeName()); @@ -83,11 +144,12 @@ void testTypeWithoutAttribute() { assertNull(date.getListName()); } + @Test @SuppressWarnings("deprecation") - void testCreateDefault_HasTheOriginalTypesWithTheirMasksOnly() { + void testCreateDefault_HasTheOriginalTypesWithTheirMasksOnly() throws InstantiationException { final SdkDataTypeRepository defaults = SdkDataTypeRepository.createDefault(); - final SdkDataTypeRepository dataTypes = new SdkDataTypeRepository(); + final SdkDataTypeRepository dataTypes = ofSdk1(); assertEquals(Set.of("text", "text-multilingual", "code", "internal-code", "id", "id-ref", "phone", "email", "url", "date", "zoned-date", "time", "zoned-time", "indicator", "integer", "number", @@ -99,4 +161,10 @@ void testCreateDefault_HasTheOriginalTypesWithTheirMasksOnly() { assertNull(dataType.getListName()); } } + + /** The constructor without arguments reads the resource of this library. */ + @Test + void testTheConstructorWithoutArgumentsReadsTheResource() { + assertEquals(23, new SdkDataTypeRepository().size()); + } } diff --git a/src/test/java/eu/europa/ted/eforms/sdk/repository/SdkFieldRepositoryTest.java b/src/test/java/eu/europa/ted/eforms/sdk/repository/SdkFieldRepositoryTest.java new file mode 100644 index 0000000..40ffb6e --- /dev/null +++ b/src/test/java/eu/europa/ted/eforms/sdk/repository/SdkFieldRepositoryTest.java @@ -0,0 +1,286 @@ +/* + * Copyright 2026 European Union + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by the European + * Commission – subsequent versions of the EUPL (the "Licence"); You may not use this work except in + * compliance with the Licence. You may obtain a copy of the Licence at: + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software distributed under the Licence + * is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the Licence for the specific language governing permissions and limitations under + * the Licence. + */ +package eu.europa.ted.eforms.sdk.repository; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import java.nio.file.Path; +import org.junit.jupiter.api.Test; +import eu.europa.ted.eforms.sdk.entity.SdkField; +import eu.europa.ted.eforms.sdk.entity.v1.SdkFieldV1; +import eu.europa.ted.eforms.sdk.entity.v2.SdkFieldV2; + +/** + * Verifies where the fields of each SDK version are read from (TEDEFO-5229): fields/fields.json for + * SDK 1, fields/fwd/fields.json for SDK 2, and the properties whose structure differs between the + * two. + */ +class SdkFieldRepositoryTest { + + /** SDK 1 publishes its fields in fields/fields.json. */ + private static final String SDK_1 = "1.16"; + + /** SDK 2 publishes its fields in fields/fwd/fields.json. */ + private static final String SDK_2 = "2.0"; + + /** + * The folders of the SDKs, one per version. The two hold different fields on purpose, so that a + * test can tell which one was read. + */ + private static final Path SDK_ROOT = Path.of("src", "test", "resources", "sdk-fixture"); + + /** Only fields/fwd/fields.json holds this field. */ + private static final String FIELD_OF_THE_FWD_FILE = "BT-Fwd-Only"; + + private static SdkFieldRepository ofSdk1() throws InstantiationException { + final Path sdk = SDK_ROOT.resolve(SDK_1); + return SdkRepositoryFactory.getSdkFieldRepository(SDK_1, sdk, + SdkRepositoryFactory.getSdkNodeRepository(SDK_1, sdk)); + } + + private static SdkFieldRepository ofSdk2() throws InstantiationException { + final Path sdk = SDK_ROOT.resolve(SDK_2); + return SdkRepositoryFactory.getSdkFieldRepository(SDK_2, sdk, + SdkRepositoryFactory.getSdkNodeRepository(SDK_2, sdk)); + } + + @Test + void testSdk1ReadsTheFieldsFromFieldsJson() throws InstantiationException { + SdkFieldRepository fields = ofSdk1(); + + assertEquals(4, fields.size()); + assertNull(fields.get(FIELD_OF_THE_FWD_FILE), + "the field of fields/fwd/fields.json must not reach SDK 1"); + } + + /** + * The point of TEDEFO-5229: with SDK 2 the fields come from fields/fwd/fields.json. The fwd file + * holds a field that fields/fields.json does not, so only it can be the source. + */ + @Test + void testSdk2ReadsTheFieldsFromTheFwdFile() throws InstantiationException { + SdkFieldRepository fields = ofSdk2(); + + assertEquals(10, fields.size()); + assertNotNull(fields.get(FIELD_OF_THE_FWD_FILE), + "the fields of SDK 2 must come from fields/fwd/fields.json"); + } + + /** SDK 2 gives repeatable as a plain boolean, SDK 1 as an object with a value. */ + @Test + void testRepeatableIsReadFromBothShapes() throws InstantiationException { + SdkFieldRepository ofSdk1 = ofSdk1(); + SdkFieldRepository ofSdk2 = ofSdk2(); + + assertTrue(ofSdk1.get("BT-Plain").isRepeatable(), "SDK 1 gives an object with a value"); + assertFalse(ofSdk1.get("BT-NotRepeatable").isRepeatable()); + + assertTrue(ofSdk2.get("BT-Plain").isRepeatable(), "SDK 2 gives a plain boolean"); + assertFalse(ofSdk2.get("BT-NotRepeatable").isRepeatable()); + } + + /** SDK 2 gives the disclosure group as the groupId of the disclosureControl block. */ + @Test + void testTheDisclosureGroupIdComesFromTheDisclosureControlForSdk2() + throws InstantiationException { + assertEquals("con-esti", ofSdk1().get("BT-Withheld").getPrivacyCode(), + "SDK 1 gives it as privacy.code"); + assertEquals("con-esti", ofSdk2().get("BT-Withheld").getPrivacyCode(), + "SDK 2 gives it as disclosureControl.groupId"); + assertNull(ofSdk2().get("BT-Plain").getPrivacyCode(), "a field that is not withheld has none"); + } + + /** + * The two EFX expressions are present on the fields that are withheld only for some of their + * instances, and are loaded as they are, without being translated. + */ + @Test + void testTheExpressionsOfTheConditionallyWithheldFields() throws InstantiationException { + SdkField withheldSometimes = ofSdk2().get("BT-WithheldSometimes"); + + assertEquals("cro-bor-law", withheldSometimes.getPrivacyCode()); + assertEquals("{BT-WithheldSometimes} ${BT-Plain == 'x'}", + withheldSometimes.getWithholdingCondition()); + assertEquals("{ND-Root} &{/BT-WithheldSometimes[BT-Plain == 'x']}", + withheldSometimes.getUndisclosedFieldSelector()); + } + + /** A field withheld for all of its instances carries no expression. */ + @Test + void testTheFieldsWithoutExpressions() throws InstantiationException { + SdkFieldRepository ofSdk2 = ofSdk2(); + + assertNull(ofSdk2.get("BT-Withheld").getWithholdingCondition()); + assertNull(ofSdk2.get("BT-Withheld").getUndisclosedFieldSelector()); + assertNull(ofSdk2.get("BT-Plain").getWithholdingCondition()); + assertNull(ofSdk2.get("BT-Plain").getUndisclosedFieldSelector()); + } + + /** + * SDK 1 names the four disclosure fields in the privacy block of every withheld field. + */ + @Test + void testSdk1NamesTheDisclosureFieldsPerField() throws InstantiationException { + SdkField.PrivacySettings ofSdk1 = ofSdk1().get("BT-Withheld").getPrivacySettings(); + + assertNotNull(ofSdk1, "SDK 1 names them in the privacy block"); + assertEquals("BT-195(BT-Withheld)-Lot", ofSdk1.getPrivacyCodeFieldId()); + assertEquals("BT-197(BT-Withheld)-Lot", ofSdk1.getJustificationCodeFieldId()); + assertEquals("BT-196(BT-Withheld)-Lot", ofSdk1.getJustificationDescriptionFieldId()); + assertEquals("BT-198(BT-Withheld)-Lot", ofSdk1.getPublicationDateFieldId()); + } + + /** + * SDK 2 names them once, by their stable identifiers, so every field that can be withheld gets + * them from there instead (TEDEFO-5230). The EFX Toolkit resolves the privacy properties of + * EFX 2 through these, so they must stay available per field. + */ + @Test + void testSdk2TakesTheDisclosureFieldsFromTheStableIds() throws InstantiationException { + SdkField.PrivacySettings ofSdk2 = ofSdk2().get("BT-Withheld").getPrivacySettings(); + + assertNotNull(ofSdk2, "a withheld field must still have its disclosure fields"); + assertEquals("BT-195-notice", ofSdk2.getPrivacyCodeFieldId()); + assertEquals("BT-197-notice", ofSdk2.getJustificationCodeFieldId()); + assertEquals("BT-196-notice", ofSdk2.getJustificationDescriptionFieldId()); + assertEquals("BT-198-notice", ofSdk2.getPublicationDateFieldId()); + } + + /** The identifiers are resolved to the fields themselves, as they are for SDK 1. */ + @Test + void testTheDisclosureFieldsAreResolvedToFields() throws InstantiationException { + SdkField.PrivacySettings ofSdk2 = ofSdk2().get("BT-Withheld").getPrivacySettings(); + + assertEquals("BT-195-notice", ofSdk2.getPrivacyCodeField().getId()); + assertEquals("BT-197-notice", ofSdk2.getJustificationCodeField().getId()); + assertEquals("BT-196-notice", ofSdk2.getJustificationDescriptionField().getId()); + assertEquals("BT-198-notice", ofSdk2.getPublicationDateField().getId()); + } + + /** Only the fields that can be withheld get them. */ + @Test + void testAFieldThatCannotBeWithheldHasNoDisclosureFields() throws InstantiationException { + assertNull(ofSdk2().get("BT-Plain").getPrivacySettings(), + "a field that is not withheld has no disclosure fields"); + } + + /** The stable identifiers of the fields are read in full, and each gives its field. */ + @Test + void testTheStableIdsOfTheFieldsAreLoaded() throws InstantiationException { + SdkFieldRepository ofSdk2 = ofSdk2(); + + assertEquals(5, ofSdk2.getStableIds().size()); + assertEquals("BT-198-notice", ofSdk2.getFieldIdByStableId("disclosureDate")); + assertEquals("OPP-070-notice", ofSdk2.getFieldIdByStableId("noticeSubType")); + } + + /** An unknown key gives no result, and is not an error. */ + @Test + void testAnUnknownStableIdGivesNothing() throws InstantiationException { + assertNull(ofSdk2().getFieldIdByStableId("thereIsNoSuchKey")); + } + + /** SDK 1 has no such map, so the lookup gives no result. */ + @Test + void testSdk1HasNoStableIds() throws InstantiationException { + SdkFieldRepository ofSdk1 = ofSdk1(); + + assertTrue(ofSdk1.getStableIds().isEmpty()); + assertNull(ofSdk1.getFieldIdByStableId("disclosureDate")); + } + + /** The fields are linked to the nodes loaded from the file of the same SDK version. */ + @Test + void testTheFieldsAreLinkedToTheirParentNode() throws InstantiationException { + assertEquals("ND-Lot", ofSdk1().get("BT-Plain").getParentNode().getId()); + assertEquals("ND-Lot", ofSdk2().get("BT-Plain").getParentNode().getId()); + } + + /** + * The nodes are optional: without them, the fields are read without their parent node, and the + * disclosure fields of SDK 2 are still attached and resolved. + */ + @Test + void testWithoutTheNodesTheFieldsHaveNoParentNode() throws InstantiationException { + final SdkFieldRepository ofSdk1 = + SdkRepositoryFactory.getSdkFieldRepository(SDK_1, SDK_ROOT.resolve(SDK_1), null); + final SdkFieldRepository ofSdk2 = + SdkRepositoryFactory.getSdkFieldRepository(SDK_2, SDK_ROOT.resolve(SDK_2), null); + + assertNull(ofSdk1.get("BT-Plain").getParentNode()); + assertNull(ofSdk2.get("BT-Plain").getParentNode()); + assertEquals("BT-198-notice", + ofSdk2.get("BT-Withheld").getPrivacySettings().getPublicationDateField().getId()); + } + + /** + * SDK 1 maps measure onto duration, because it had no duration type. SDK 2 separates the two, so + * it must report the type the SDK declares. + */ + @Test + void testMeasureIsOnlyMappedOntoDurationForSdk1() throws InstantiationException { + assertEquals("duration", ofSdk1().get("BT-Measure").getType(), + "SDK 1 has no duration type, so measure means duration"); + assertEquals("measure", ofSdk2().get("BT-Measure").getType(), + "SDK 2 separates duration from measure"); + } + + /** Each SDK version gets its own field implementation, as before. */ + @Test + void testEachSdkVersionGetsItsOwnFields() throws InstantiationException { + SdkField ofSdk1 = ofSdk1().get("BT-Plain"); + SdkField ofSdk2 = ofSdk2().get("BT-Plain"); + + assertTrue(ofSdk1 instanceof SdkFieldV1, "SDK 1 gives " + ofSdk1.getClass()); + assertTrue(ofSdk2 instanceof SdkFieldV2, "SDK 2 gives " + ofSdk2.getClass()); + assertEquals("plain", ((SdkFieldV2) ofSdk2).getAlias(), "SDK 2 also reads the alias"); + } + + /** The constructor that takes a fields.json reads the file it is given. */ + @Test + void testTheConstructorReadsTheFileItIsGiven() throws InstantiationException { + SdkFieldRepository fields = new SdkFieldRepository(SDK_1, + SDK_ROOT.resolve(Path.of("1.16", "fields", "fields.json"))); + + assertEquals(4, fields.size()); + assertEquals("con-esti", fields.get("BT-Withheld").getPrivacyCode()); + } + + /** + * The EFX Toolkit still reads the fields of SDK 2 from fields/fields.json, with the constructor + * that takes the file. There an SDK 2 field takes repeatable from the object, and its privacy + * code and disclosure fields from its privacy block. + */ + @Test + void testSdk2FieldsJsonIsReadWithItsPrivacyBlocks() throws InstantiationException { + final Path fieldsJson = SDK_ROOT.resolve(Path.of(SDK_2, "fields", "fields.json")); + final SdkFieldRepository fields = new SdkFieldRepository(SDK_2, fieldsJson, + new SdkNodeRepository(SDK_2, fieldsJson)); + + assertTrue(fields.get("BT-Plain") instanceof SdkFieldV2); + assertTrue(fields.get("BT-Plain").isRepeatable()); + assertFalse(fields.get("BT-Withheld").isRepeatable()); + assertEquals("con-esti", fields.get("BT-Withheld").getPrivacyCode()); + final SdkField.PrivacySettings privacy = fields.get("BT-Withheld").getPrivacySettings(); + assertEquals("BT-195-notice", privacy.getPrivacyCodeField().getId()); + assertEquals("BT-197-notice", privacy.getJustificationCodeField().getId()); + assertEquals("BT-196-notice", privacy.getJustificationDescriptionField().getId()); + assertEquals("BT-198-notice", privacy.getPublicationDateField().getId()); + assertEquals("measure", fields.get("BT-Measure").getType(), + "SDK 2 separates duration from measure, whichever file it is read from"); + } +} diff --git a/src/test/java/eu/europa/ted/eforms/sdk/repository/SdkNodeRepositoryTest.java b/src/test/java/eu/europa/ted/eforms/sdk/repository/SdkNodeRepositoryTest.java new file mode 100644 index 0000000..c80147b --- /dev/null +++ b/src/test/java/eu/europa/ted/eforms/sdk/repository/SdkNodeRepositoryTest.java @@ -0,0 +1,151 @@ +/* + * Copyright 2026 European Union + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by the European + * Commission – subsequent versions of the EUPL (the "Licence"); You may not use this work except in + * compliance with the Licence. You may obtain a copy of the Licence at: + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software distributed under the Licence + * is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the Licence for the specific language governing permissions and limitations under + * the Licence. + */ +package eu.europa.ted.eforms.sdk.repository; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import java.nio.file.Path; +import java.util.Arrays; +import org.junit.jupiter.api.Test; +import eu.europa.ted.eforms.sdk.entity.SdkNode; +import eu.europa.ted.eforms.sdk.entity.v1.SdkNodeV1; +import eu.europa.ted.eforms.sdk.entity.v2.SdkNodeV2; + +/** + * Verifies where the nodes of each SDK version are read from (TEDEFO-5228): the xmlStructure array + * of fields/fields.json for SDK 1, the nodes array of fields/fwd/nodes.json for SDK 2. + */ +class SdkNodeRepositoryTest { + + /** SDK 1 publishes its nodes in fields/fields.json. */ + private static final String SDK_1 = "1.16"; + + /** SDK 2 publishes its nodes in fields/fwd/nodes.json. */ + private static final String SDK_2 = "2.0"; + + /** + * The folders of the SDKs, one per version. The two hold different nodes on purpose, so that a + * test can tell which one was read. + */ + private static final Path SDK_ROOT = Path.of("src", "test", "resources", "sdk-fixture"); + + /** Only fields/fwd/nodes.json holds this node. */ + private static final String NODE_OF_THE_FWD_FILE = "ND-Fwd-Only"; + + @Test + void testSdk1ReadsTheNodesFromFieldsJson() throws InstantiationException { + SdkNodeRepository nodes = SdkRepositoryFactory.getSdkNodeRepository(SDK_1, SDK_ROOT.resolve(SDK_1)); + + assertEquals(2, nodes.size()); + assertEquals("/*/cac:ProcurementProjectLot", nodes.get("ND-Lot").getXpathAbsolute()); + assertNull(nodes.get(NODE_OF_THE_FWD_FILE), + "the node of fields/fwd/nodes.json must not reach SDK 1"); + } + + /** + * The point of TEDEFO-5228: with SDK 2 the nodes come from fields/fwd/nodes.json. The fwd file + * holds a node that fields/fields.json does not, so only it can be the source. + */ + @Test + void testSdk2ReadsTheNodesFromTheFwdFile() throws InstantiationException { + SdkNodeRepository nodes = SdkRepositoryFactory.getSdkNodeRepository(SDK_2, SDK_ROOT.resolve(SDK_2)); + + assertEquals(3, nodes.size()); + assertNotNull(nodes.get(NODE_OF_THE_FWD_FILE), + "the nodes of SDK 2 must come from fields/fwd/nodes.json"); + assertEquals("/*/cac:ProcurementProjectLot/cac:FwdOnly", + nodes.get(NODE_OF_THE_FWD_FILE).getXpathAbsolute()); + } + + /** The nodes have the same properties in both files, so the node entities do not change. */ + @Test + void testTheNodePropertiesAreTheSameInBothFiles() throws InstantiationException { + SdkNode ofSdk1 = SdkRepositoryFactory.getSdkNodeRepository(SDK_1, SDK_ROOT.resolve(SDK_1)).get("ND-Lot"); + SdkNode ofSdk2 = SdkRepositoryFactory.getSdkNodeRepository(SDK_2, SDK_ROOT.resolve(SDK_2)).get("ND-Lot"); + + assertEquals(ofSdk1.getId(), ofSdk2.getId()); + assertEquals(ofSdk1.getParentId(), ofSdk2.getParentId()); + assertEquals(ofSdk1.getXpathAbsolute(), ofSdk2.getXpathAbsolute()); + assertEquals(ofSdk1.getXpathRelative(), ofSdk2.getXpathRelative()); + assertEquals(ofSdk1.isRepeatable(), ofSdk2.isRepeatable()); + assertTrue(ofSdk2.isRepeatable(), "the fixture's lot is repeatable"); + } + + /** The parents are wired whichever file the nodes come from, so the ancestry resolves. */ + @Test + void testTheParentsAreWiredForBothVersions() throws InstantiationException { + SdkNodeRepository ofSdk1 = SdkRepositoryFactory.getSdkNodeRepository(SDK_1, SDK_ROOT.resolve(SDK_1)); + SdkNodeRepository ofSdk2 = SdkRepositoryFactory.getSdkNodeRepository(SDK_2, SDK_ROOT.resolve(SDK_2)); + + assertEquals("ND-Root", ofSdk1.get("ND-Lot").getParent().getId()); + assertNull(ofSdk1.get("ND-Root").getParent(), "the root has no parent"); + + // The fwd file declares its nodes before their parents in one case, which the second pass wires. + assertEquals("ND-Lot", ofSdk2.get(NODE_OF_THE_FWD_FILE).getParent().getId()); + assertEquals(Arrays.asList(NODE_OF_THE_FWD_FILE, "ND-Lot", "ND-Root"), + ofSdk2.get(NODE_OF_THE_FWD_FILE).getAncestry()); + } + + /** The stable identifiers of the nodes are read in full, and each gives its node. */ + @Test + void testTheStableIdsOfTheNodesAreLoaded() throws InstantiationException { + SdkNodeRepository ofSdk2 = SdkRepositoryFactory.getSdkNodeRepository(SDK_2, SDK_ROOT.resolve(SDK_2)); + + assertEquals(2, ofSdk2.getStableIds().size()); + assertEquals("ND-Root", ofSdk2.getNodeIdByStableId("root")); + assertEquals("ND-Organizations", ofSdk2.getNodeIdByStableId("organizations")); + assertNotNull(ofSdk2.get(ofSdk2.getNodeIdByStableId("root")), + "the key must give an identifier that the nodes can be looked up by"); + } + + /** An unknown key gives no result, and is not an error. */ + @Test + void testAnUnknownStableIdGivesNothing() throws InstantiationException { + assertNull(SdkRepositoryFactory.getSdkNodeRepository(SDK_2, SDK_ROOT.resolve(SDK_2)).getNodeIdByStableId("thereIsNoSuchKey")); + } + + /** SDK 1 has no such map, so the lookup gives no result. */ + @Test + void testSdk1HasNoStableIds() throws InstantiationException { + SdkNodeRepository ofSdk1 = SdkRepositoryFactory.getSdkNodeRepository(SDK_1, SDK_ROOT.resolve(SDK_1)); + + assertTrue(ofSdk1.getStableIds().isEmpty()); + assertNull(ofSdk1.getNodeIdByStableId("root")); + } + + /** Each SDK version gets its own node implementation, as before. */ + @Test + void testEachSdkVersionGetsItsOwnNodes() throws InstantiationException { + SdkNode ofSdk1 = SdkRepositoryFactory.getSdkNodeRepository(SDK_1, SDK_ROOT.resolve(SDK_1)).get("ND-Root"); + SdkNode ofSdk2 = SdkRepositoryFactory.getSdkNodeRepository(SDK_2, SDK_ROOT.resolve(SDK_2)).get("ND-Root"); + + assertTrue(ofSdk1 instanceof SdkNodeV1, "SDK 1 gives " + ofSdk1.getClass()); + assertTrue(ofSdk2 instanceof SdkNodeV2, "SDK 2 gives " + ofSdk2.getClass()); + assertEquals("root", ((SdkNodeV2) ofSdk2).getAlias(), "SDK 2 also reads the alias"); + } + + /** + * The constructor that takes a fields.json reads the xmlStructure array of the file it is given. + */ + @Test + void testTheConstructorReadsTheXmlStructureOfTheFileItIsGiven() throws InstantiationException { + SdkNodeRepository nodes = new SdkNodeRepository(SDK_1, + SDK_ROOT.resolve(Path.of("1.16", "fields", "fields.json"))); + + assertEquals(2, nodes.size()); + assertEquals("ND-Root", nodes.get("ND-Lot").getParent().getId()); + } +} diff --git a/src/test/resources/sdk-fixture/1.16/fields/fields.json b/src/test/resources/sdk-fixture/1.16/fields/fields.json new file mode 100644 index 0000000..04c5904 --- /dev/null +++ b/src/test/resources/sdk-fixture/1.16/fields/fields.json @@ -0,0 +1,63 @@ +{ + "sdkVersion" : "eforms-sdk-1.16.0", + "xmlStructure" : [ { + "id" : "ND-Root", + "xpathAbsolute" : "/*", + "xpathRelative" : "/*", + "repeatable" : false + }, { + "id" : "ND-Lot", + "parentId" : "ND-Root", + "xpathAbsolute" : "/*/cac:ProcurementProjectLot", + "xpathRelative" : "cac:ProcurementProjectLot", + "repeatable" : true + } ], + "fields" : [ { + "id" : "BT-Plain", + "parentNodeId" : "ND-Lot", + "xpathAbsolute" : "/*/cac:ProcurementProjectLot/cbc:Plain", + "xpathRelative" : "cbc:Plain", + "type" : "text", + "repeatable" : { + "value" : true, + "severity" : "ERROR" + } + }, { + "id" : "BT-NotRepeatable", + "parentNodeId" : "ND-Lot", + "xpathAbsolute" : "/*/cac:ProcurementProjectLot/cbc:NotRepeatable", + "xpathRelative" : "cbc:NotRepeatable", + "type" : "text", + "repeatable" : { + "value" : false, + "severity" : "ERROR" + } + }, { + "id" : "BT-Withheld", + "parentNodeId" : "ND-Lot", + "xpathAbsolute" : "/*/cac:ProcurementProjectLot/cbc:Withheld", + "xpathRelative" : "cbc:Withheld", + "type" : "text", + "repeatable" : { + "value" : false, + "severity" : "ERROR" + }, + "privacy" : { + "code" : "con-esti", + "unpublishedFieldId" : "BT-195(BT-Withheld)-Lot", + "reasonCodeFieldId" : "BT-197(BT-Withheld)-Lot", + "reasonDescriptionFieldId" : "BT-196(BT-Withheld)-Lot", + "publicationDateFieldId" : "BT-198(BT-Withheld)-Lot" + } + }, { + "id" : "BT-Measure", + "parentNodeId" : "ND-Lot", + "xpathAbsolute" : "/*/cac:ProcurementProjectLot/cbc:Measure", + "xpathRelative" : "cbc:Measure", + "type" : "measure", + "repeatable" : { + "value" : false, + "severity" : "ERROR" + } + } ] +} diff --git a/src/test/resources/sdk-fixture/2.0/fields/fields.json b/src/test/resources/sdk-fixture/2.0/fields/fields.json new file mode 100644 index 0000000..d9c9d92 --- /dev/null +++ b/src/test/resources/sdk-fixture/2.0/fields/fields.json @@ -0,0 +1,93 @@ +{ + "sdkVersion" : "eforms-sdk-2.0.0", + "xmlStructure" : [ { + "id" : "ND-Root", + "xpathAbsolute" : "/*", + "xpathRelative" : "/*", + "repeatable" : false + }, { + "id" : "ND-Lot", + "parentId" : "ND-Root", + "xpathAbsolute" : "/*/cac:ProcurementProjectLot", + "xpathRelative" : "cac:ProcurementProjectLot", + "repeatable" : true + } ], + "fields" : [ { + "id" : "BT-Plain", + "parentNodeId" : "ND-Lot", + "xpathAbsolute" : "/*/cac:ProcurementProjectLot/cbc:Plain", + "xpathRelative" : "cbc:Plain", + "type" : "text", + "repeatable" : { + "value" : true, + "severity" : "ERROR" + } + }, { + "id" : "BT-Withheld", + "parentNodeId" : "ND-Lot", + "xpathAbsolute" : "/*/cac:ProcurementProjectLot/cbc:Withheld", + "xpathRelative" : "cbc:Withheld", + "type" : "text", + "repeatable" : { + "value" : false, + "severity" : "ERROR" + }, + "privacy" : { + "code" : "con-esti", + "unpublishedFieldId" : "BT-195-notice", + "reasonCodeFieldId" : "BT-197-notice", + "reasonDescriptionFieldId" : "BT-196-notice", + "publicationDateFieldId" : "BT-198-notice" + } + }, { + "id" : "BT-195-notice", + "parentNodeId" : "ND-Root", + "xpathAbsolute" : "/*/efac:NoticeInformation/efac:Nondisclosure/efbc:NondisclosureID", + "xpathRelative" : "efac:NoticeInformation/efac:Nondisclosure/efbc:NondisclosureID", + "type" : "id", + "repeatable" : { + "value" : true, + "severity" : "ERROR" + } + }, { + "id" : "BT-196-notice", + "parentNodeId" : "ND-Root", + "xpathAbsolute" : "/*/efac:NoticeInformation/cbc:ReasonDescription", + "xpathRelative" : "efac:NoticeInformation/cbc:ReasonDescription", + "type" : "text-multilingual", + "repeatable" : { + "value" : false, + "severity" : "ERROR" + } + }, { + "id" : "BT-197-notice", + "parentNodeId" : "ND-Root", + "xpathAbsolute" : "/*/efac:NoticeInformation/cbc:ReasonCode", + "xpathRelative" : "efac:NoticeInformation/cbc:ReasonCode", + "type" : "code", + "repeatable" : { + "value" : false, + "severity" : "ERROR" + } + }, { + "id" : "BT-198-notice", + "parentNodeId" : "ND-Root", + "xpathAbsolute" : "/*/efac:NoticeInformation/efbc:PublicationDate", + "xpathRelative" : "efac:NoticeInformation/efbc:PublicationDate", + "type" : "date", + "repeatable" : { + "value" : false, + "severity" : "ERROR" + } + }, { + "id" : "BT-Measure", + "parentNodeId" : "ND-Lot", + "xpathAbsolute" : "/*/cac:ProcurementProjectLot/cbc:Measure", + "xpathRelative" : "cbc:Measure", + "type" : "measure", + "repeatable" : { + "value" : false, + "severity" : "ERROR" + } + } ] +} diff --git a/src/test/resources/sdk-fixture/2.0/fields/fwd/data-types.json b/src/test/resources/sdk-fixture/2.0/fields/fwd/data-types.json new file mode 100644 index 0000000..f05d2a9 --- /dev/null +++ b/src/test/resources/sdk-fixture/2.0/fields/fwd/data-types.json @@ -0,0 +1,45 @@ +{ + "dataTypes" : [ { + "type" : "amount", + "description" : "Monetary value with currency", + "valueType" : "decimal", + "maskingValue" : "undisclosed", + "displayTypes" : [ "TEXTBOX" ], + "attribute" : { + "name" : "currencyID", + "description" : "ISO 4217 currency code", + "type" : "currency" + } + }, { + "type" : "currency", + "description" : "ISO 4217 currency code", + "valueType" : "string", + "maskingValue" : "undisclosed", + "displayTypes" : [ "COMBOBOX" ], + "listName" : "currency" + }, { + "type" : "duration", + "description" : "Duration", + "valueType" : "decimal", + "maskingValue" : "undisclosed", + "displayTypes" : [ "TEXTBOX" ], + "attribute" : { + "name" : "unitCode", + "description" : "UN/ECE Recommendation 20 unit code", + "type" : "duration-unit" + } + }, { + "type" : "duration-unit", + "description" : "Unit of duration", + "valueType" : "string", + "maskingValue" : "undisclosed", + "displayTypes" : [ "COMBOBOX" ], + "listName" : "timeperiod" + }, { + "type" : "date", + "description" : "Date value with timezone", + "valueType" : "date", + "maskingValue" : "1970-01-01Z", + "displayTypes" : [ "TEXTBOX" ] + } ] +} diff --git a/src/test/resources/sdk-fixture/2.0/fields/fwd/fields.json b/src/test/resources/sdk-fixture/2.0/fields/fwd/fields.json new file mode 100644 index 0000000..df6bf67 --- /dev/null +++ b/src/test/resources/sdk-fixture/2.0/fields/fwd/fields.json @@ -0,0 +1,89 @@ +{ + "specialPurpose" : { + "disclosureDate" : "BT-198-notice", + "disclosureGroup" : "BT-195-notice", + "disclosureJustificationCode" : "BT-197-notice", + "disclosureJustificationDescription" : "BT-196-notice", + "noticeSubType" : "OPP-070-notice" + }, + "fields" : [ { + "id" : "BT-Plain", + "parentNodeId" : "ND-Lot", + "xpathAbsolute" : "/*/cac:ProcurementProjectLot/cbc:Plain", + "xpathRelative" : "cbc:Plain", + "type" : "text", + "repeatable" : true, + "alias" : "plain" + }, { + "id" : "BT-NotRepeatable", + "parentNodeId" : "ND-Lot", + "xpathAbsolute" : "/*/cac:ProcurementProjectLot/cbc:NotRepeatable", + "xpathRelative" : "cbc:NotRepeatable", + "type" : "text", + "repeatable" : false + }, { + "id" : "BT-Withheld", + "parentNodeId" : "ND-Lot", + "xpathAbsolute" : "/*/cac:ProcurementProjectLot/cbc:Withheld", + "xpathRelative" : "cbc:Withheld", + "type" : "text", + "repeatable" : false, + "disclosureControl" : { + "groupId" : "con-esti" + } + }, { + "id" : "BT-WithheldSometimes", + "parentNodeId" : "ND-Lot", + "xpathAbsolute" : "/*/cac:ProcurementProjectLot/cbc:WithheldSometimes", + "xpathRelative" : "cbc:WithheldSometimes", + "type" : "text", + "repeatable" : false, + "disclosureControl" : { + "groupId" : "cro-bor-law", + "withholdingCondition" : "{BT-WithheldSometimes} ${BT-Plain == 'x'}", + "undisclosedFieldSelector" : "{ND-Root} &{/BT-WithheldSometimes[BT-Plain == 'x']}" + } + }, { + "id" : "BT-Measure", + "parentNodeId" : "ND-Lot", + "xpathAbsolute" : "/*/cac:ProcurementProjectLot/cbc:Measure", + "xpathRelative" : "cbc:Measure", + "type" : "measure", + "repeatable" : false + }, { + "id" : "BT-Fwd-Only", + "parentNodeId" : "ND-Lot", + "xpathAbsolute" : "/*/cac:ProcurementProjectLot/cbc:FwdOnly", + "xpathRelative" : "cbc:FwdOnly", + "type" : "text", + "repeatable" : false + }, { + "id" : "BT-195-notice", + "parentNodeId" : "ND-Root", + "xpathAbsolute" : "/*/efac:FieldsPrivacy/efbc:FieldIdentifierCode", + "xpathRelative" : "efac:FieldsPrivacy/efbc:FieldIdentifierCode", + "type" : "code", + "repeatable" : false + }, { + "id" : "BT-196-notice", + "parentNodeId" : "ND-Root", + "xpathAbsolute" : "/*/efac:FieldsPrivacy/efbc:ReasonDescription", + "xpathRelative" : "efac:FieldsPrivacy/efbc:ReasonDescription", + "type" : "text", + "repeatable" : false + }, { + "id" : "BT-197-notice", + "parentNodeId" : "ND-Root", + "xpathAbsolute" : "/*/efac:FieldsPrivacy/cbc:ReasonCode", + "xpathRelative" : "efac:FieldsPrivacy/cbc:ReasonCode", + "type" : "code", + "repeatable" : false + }, { + "id" : "BT-198-notice", + "parentNodeId" : "ND-Root", + "xpathAbsolute" : "/*/efac:FieldsPrivacy/efbc:PublicationDate", + "xpathRelative" : "efac:FieldsPrivacy/efbc:PublicationDate", + "type" : "date", + "repeatable" : false + } ] +} diff --git a/src/test/resources/sdk-fixture/2.0/fields/fwd/nodes.json b/src/test/resources/sdk-fixture/2.0/fields/fwd/nodes.json new file mode 100644 index 0000000..adc565a --- /dev/null +++ b/src/test/resources/sdk-fixture/2.0/fields/fwd/nodes.json @@ -0,0 +1,26 @@ +{ + "specialPurpose" : { + "root" : "ND-Root", + "organizations" : "ND-Organizations" + }, + "nodes" : [ { + "id" : "ND-Root", + "xpathAbsolute" : "/*", + "xpathRelative" : "/*", + "repeatable" : false, + "alias" : "root" + }, { + "id" : "ND-Fwd-Only", + "parentId" : "ND-Lot", + "xpathAbsolute" : "/*/cac:ProcurementProjectLot/cac:FwdOnly", + "xpathRelative" : "cac:FwdOnly", + "repeatable" : false + }, { + "id" : "ND-Lot", + "parentId" : "ND-Root", + "xpathAbsolute" : "/*/cac:ProcurementProjectLot", + "xpathRelative" : "cac:ProcurementProjectLot", + "repeatable" : true, + "alias" : "lot" + } ] +}