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+ * 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+ * 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+ * 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+ * 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+ * 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+ * 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+ * 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+ * 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" + } ] +}