From 104568728ca7061e5653b61a2c934876c1cb728b Mon Sep 17 00:00:00 2001 From: ROUSSY Christophe Date: Thu, 8 Oct 2026 11:08:22 +0200 Subject: [PATCH 01/13] TEDEFO-5231 Read the SDK 2 data types from fields/fwd/data-types.json MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The data types were always read from the data-types.json resource of this library, whatever the SDK version. SDK 2 publishes them in fields/fwd/data-types.json, so reading them from there gives the EFX Toolkit the masking values, attributes and code lists of the SDK instead of a copy kept here. SDK 1 publishes none, so it keeps reading the resource. - Add SdkDataTypeRepository(String sdkVersion, Path sdkRootPath), which decides where the data types come from: the resource for SDK 1, fields/fwd/data-types.json (SdkResource.FIELDS_FWD_DATA_TYPES, already defined) for SDK 2. The file has the same format as the resource, so the loop that creates the data types is the same for both. The version is decided here so that callers do not have to check it. - Give each SDK version its own data type, as the other entities that differ per version have: SdkComponentType.DATA_TYPE, SdkEntityFactory.getSdkDataType(String, JsonNode), and the SdkDataTypeV1/SdkDataTypeV2 implementations. SdkDataTypeV2 adds nothing yet; it exists so that the two versions can diverge without changing the callers. - Deprecate the no-arg constructor in favour of the version-aware one. It, createDefault() and SdkDataType itself are left as they were, so the existing callers (SdkSymbolResolver, SdkAnalyzerSymbolResolver) are unaffected and the change stays binary compatible. Tests: the existing assertions now run through the SDK 1 path, which pins that nothing changes for SDK 1. New tests show that SDK 2 reads the data types, their attributes and their code lists from the SDK — the fixture's masking value differs from the resource's, so only the file can be the source — that the properties a data type does not read are ignored, and that each version gets its own implementation. --- .../sdk/component/SdkComponentType.java | 2 +- .../ted/eforms/sdk/entity/SdkDataType.java | 12 +- .../eforms/sdk/entity/SdkEntityFactory.java | 6 + .../eforms/sdk/entity/v1/SdkDataTypeV1.java | 35 ++++++ .../eforms/sdk/entity/v2/SdkDataTypeV2.java | 40 +++++++ .../sdk/repository/SdkDataTypeRepository.java | 66 ++++++++++- .../repository/SdkDataTypeRepositoryTest.java | 109 +++++++++++++++--- .../2.0/fields/fwd/data-types.json | 45 ++++++++ 8 files changed, 291 insertions(+), 24 deletions(-) create mode 100644 src/main/java/eu/europa/ted/eforms/sdk/entity/v1/SdkDataTypeV1.java create mode 100644 src/main/java/eu/europa/ted/eforms/sdk/entity/v2/SdkDataTypeV2.java create mode 100644 src/test/resources/sdk-fixture/2.0/fields/fwd/data-types.json 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..d6f9a08 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, DATA_TYPE, 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..5a9a2a2 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,16 @@ * 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 (TEDEFO-5231). The + * version-specific implementations are + * {@link eu.europa.ted.eforms.sdk.entity.v1.SdkDataTypeV1} and + * {@link eu.europa.ted.eforms.sdk.entity.v2.SdkDataTypeV2}, selected by + * {@link SdkEntityFactory#getSdkDataType(String, JsonNode)}. + *

*/ public class SdkDataType { private final String id; diff --git a/src/main/java/eu/europa/ted/eforms/sdk/entity/SdkEntityFactory.java b/src/main/java/eu/europa/ted/eforms/sdk/entity/SdkEntityFactory.java index 09705d9..6612a13 100644 --- a/src/main/java/eu/europa/ted/eforms/sdk/entity/SdkEntityFactory.java +++ b/src/main/java/eu/europa/ted/eforms/sdk/entity/SdkEntityFactory.java @@ -36,4 +36,10 @@ public static SdkNoticeSubtype getSdkNoticeSubtype(final String sdkVersion, fina return SdkEntityFactory.INSTANCE.getComponentImpl(sdkVersion, SdkComponentType.NOTICE_TYPE, SdkNoticeSubtype.class, json); } + + public static SdkDataType getSdkDataType(final String sdkVersion, final JsonNode json) + throws InstantiationException { + return SdkEntityFactory.INSTANCE.getComponentImpl(sdkVersion, SdkComponentType.DATA_TYPE, + SdkDataType.class, json); + } } diff --git a/src/main/java/eu/europa/ted/eforms/sdk/entity/v1/SdkDataTypeV1.java b/src/main/java/eu/europa/ted/eforms/sdk/entity/v1/SdkDataTypeV1.java new file mode 100644 index 0000000..535fce4 --- /dev/null +++ b/src/main/java/eu/europa/ted/eforms/sdk/entity/v1/SdkDataTypeV1.java @@ -0,0 +1,35 @@ +/* + * 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.entity.v1; + +import com.fasterxml.jackson.databind.JsonNode; +import eu.europa.ted.eforms.sdk.component.SdkComponent; +import eu.europa.ted.eforms.sdk.component.SdkComponentType; +import eu.europa.ted.eforms.sdk.entity.SdkDataType; + +/** + * An SDK 1 data type. SDK 1 publishes no data types of its own, so these are read from the + * data-types.json resource of this library (TEDEFO-5231). + */ +@SdkComponent(versions = {"1"}, componentType = SdkComponentType.DATA_TYPE) +public class SdkDataTypeV1 extends SdkDataType { + + public SdkDataTypeV1(final String id, final String privacyMask) { + super(id, privacyMask); + } + + public SdkDataTypeV1(final JsonNode dataType) { + super(dataType); + } +} diff --git a/src/main/java/eu/europa/ted/eforms/sdk/entity/v2/SdkDataTypeV2.java b/src/main/java/eu/europa/ted/eforms/sdk/entity/v2/SdkDataTypeV2.java new file mode 100644 index 0000000..984b046 --- /dev/null +++ b/src/main/java/eu/europa/ted/eforms/sdk/entity/v2/SdkDataTypeV2.java @@ -0,0 +1,40 @@ +/* + * 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.entity.v2; + +import com.fasterxml.jackson.databind.JsonNode; +import eu.europa.ted.eforms.sdk.component.SdkComponent; +import eu.europa.ted.eforms.sdk.component.SdkComponentType; +import eu.europa.ted.eforms.sdk.entity.v1.SdkDataTypeV1; + +/** + * An SDK 2 data type, read from fields/fwd/data-types.json of the SDK (TEDEFO-5231). + * + *

+ * The file has the same format as the SDK 1 resource for the properties this entity reads, so + * there is nothing to add here yet. The class exists so that the two versions can diverge without + * changing the callers, as for the other entities that differ per version. + *

+ */ +@SdkComponent(versions = {"2"}, componentType = SdkComponentType.DATA_TYPE) +public class SdkDataTypeV2 extends SdkDataTypeV1 { + + public SdkDataTypeV2(final String id, final String privacyMask) { + super(id, privacyMask); + } + + public SdkDataTypeV2(final JsonNode dataType) { + super(dataType); + } +} 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..7c41303 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,27 +16,63 @@ 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; import com.fasterxml.jackson.databind.ObjectMapper; import eu.europa.ted.eforms.sdk.SdkConstants; +import eu.europa.ted.eforms.sdk.SdkVersion; import eu.europa.ted.eforms.sdk.entity.SdkDataType; +import eu.europa.ted.eforms.sdk.entity.SdkEntityFactory; +import eu.europa.ted.eforms.sdk.resource.SdkResourceLoader; /** * Repository of SDK data types. * - * Currently uses hardcoded type definitions, in the data-types.json resource of this library. + *

+ * SDK 1 publishes no data types, so they are read from the data-types.json resource of this + * library. SDK 2 publishes them in fields/fwd/data-types.json, which has the same format, so for + * SDK 2 they are read from the SDK itself (TEDEFO-5231). This repository decides where the data + * types of each SDK version come from, so that its callers do not have to check 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. + * The major version of the only SDK that publishes no data types of its own. */ + private static final String SDK_MAJOR_WITHOUT_DATA_TYPES = "1"; + + /** + * Creates the repository of the data types of the given SDK, with their privacy masks, the + * attributes that their fields carry, and the code lists of their values. + * + * @param sdkVersion the target SDK version, which decides where the data types are read from + * @param sdkRootPath path of the root SDK folder, used for the SDK versions that publish their + * data types; may be null for SDK 1, which does not + * @throws InstantiationException if a data type cannot be created for this SDK version + */ + public SdkDataTypeRepository(final String sdkVersion, final Path sdkRootPath) + throws InstantiationException { + // The file of the SDK has the same format as the resource, so the loop is the same for both. + for (final JsonNode dataType : readDataTypes(sdkVersion, sdkRootPath) + .get(SdkConstants.DATA_TYPES_JSON_DATA_TYPES_KEY)) { + final SdkDataType sdkDataType = SdkEntityFactory.getSdkDataType(sdkVersion, dataType); + this.put(sdkDataType.getId(), sdkDataType); + } + } + + /** + * Creates the repository of the data types from the data-types.json resource of this library, + * whatever the SDK version. + * + * @deprecated Use {@link #SdkDataTypeRepository(String, Path)}, which reads the data types of + * SDK 2 from the SDK instead of this copy. + */ + @Deprecated public SdkDataTypeRepository() { for (final JsonNode dataType : readResource().get(SdkConstants.DATA_TYPES_JSON_DATA_TYPES_KEY)) { final SdkDataType sdkDataType = new SdkDataType(dataType); @@ -86,6 +122,18 @@ private void addType(String id, String privacyMask) { this.put(id, new SdkDataType(id, privacyMask)); } + /** + * @return the data types of the given SDK version, from the SDK itself when it publishes them, + * and from the resource of this library for the SDK versions that do not + */ + private static JsonNode readDataTypes(final String sdkVersion, final Path sdkRootPath) { + if (SDK_MAJOR_WITHOUT_DATA_TYPES.equals(new SdkVersion(sdkVersion).getMajor())) { + return readResource(); + } + return readSdkFile(SdkResourceLoader.getResourceAsPath(sdkVersion, + SdkConstants.SdkResource.FIELDS_FWD_DATA_TYPES, sdkRootPath)); + } + private static JsonNode readResource() { try (InputStream input = SdkDataTypeRepository.class.getResourceAsStream(SdkConstants.DATA_TYPES_JSON_FILE_NAME)) { @@ -94,4 +142,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/test/java/eu/europa/ted/eforms/sdk/repository/SdkDataTypeRepositoryTest.java b/src/test/java/eu/europa/ted/eforms/sdk/repository/SdkDataTypeRepositoryTest.java index 8c66b92..336a2fc 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,41 @@ 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; +import eu.europa.ted.eforms.sdk.entity.v1.SdkDataTypeV1; +import eu.europa.ted.eforms.sdk.entity.v2.SdkDataTypeV2; 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"; + + /** An SDK folder holding only the data types of SDK 2, 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_1, null); + } + + private static SdkDataTypeRepository ofSdk2() throws InstantiationException { + return new SdkDataTypeRepository(SDK_2, SDK_ROOT); + } + @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 +57,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 +84,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 +95,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 +143,21 @@ void testTypeWithoutAttribute() { assertNull(date.getListName()); } + /** Each SDK version gets its own data type implementation, as the other entities do. */ + @Test + void testEachSdkVersionGetsItsOwnDataTypes() throws InstantiationException { + final SdkDataType ofSdk1 = ofSdk1().get("amount"); + final SdkDataType ofSdk2 = ofSdk2().get("amount"); + + assertTrue(ofSdk1 instanceof SdkDataTypeV1, "SDK 1 gives " + ofSdk1.getClass()); + assertTrue(ofSdk2 instanceof SdkDataTypeV2, "SDK 2 gives " + ofSdk2.getClass()); + } + @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 +169,11 @@ void testCreateDefault_HasTheOriginalTypesWithTheirMasksOnly() { assertNull(dataType.getListName()); } } + + /** The deprecated constructor still reads the resource, whatever the SDK version. */ + @Test + @SuppressWarnings("deprecation") + void testTheDeprecatedConstructorStillReadsTheResource() { + assertEquals(23, new SdkDataTypeRepository().size()); + } } 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" ] + } ] +} From 96d7e60e6ceb66779716923a7bb58ec192a2d001 Mon Sep 17 00:00:00 2001 From: ROUSSY Christophe Date: Thu, 8 Oct 2026 11:14:24 +0200 Subject: [PATCH 02/13] TEDEFO-5228 Read the SDK 2 nodes from fields/fwd/nodes.json MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The nodes of both SDK 1 and SDK 2 were read from the xmlStructure array of fields/fields.json. SDK 2 publishes its nodes in the nodes array of fields/fwd/nodes.json, in the forward-compatible structure planned for SDK 3, so for SDK 2 they are now read from there. SDK 1 keeps reading fields/fields.json. The nodes have the same properties in both files, so the node entities do not change. - Add SdkNodeRepository.forSdk(String sdkVersion, Path sdkRootPath), which decides which file the nodes come from: fields/fields.json (xmlStructure) for SDK 1, fields/fwd/nodes.json (nodes) for SDK 2, using the SdkResource constants that already exist. The version is decided here so that callers do not have to check it. - The array key travels to populateMap as context, the mechanism MapFromJson already offers and SdkFieldRepository already uses: it has to be known while the map is being populated, which happens during the call to super. Keying it off the SDK version instead would have broken the existing constructor for SDK 2, which is given a fields.json. - Deprecate the (sdkVersion, jsonPath) constructor in favour of forSdk. It still reads the xmlStructure array of the file it is given, so the existing callers (SdkSymbolResolver, SdkAnalyzerSymbolResolver) are unaffected and the change stays binary compatible. - Add SdkConstants.NODES_JSON_NODES_KEY for the array of the fwd file. Tests: a new SdkNodeRepositoryTest over an SDK folder holding both files, which hold different nodes on purpose — the fwd file has a node that fields.json does not, so the tests can tell which file was read. They cover both sources, that the node properties agree between the two, that the parents are wired either way (including the second pass, for a node declared before its parent), that each version still gets its own node implementation, and that the deprecated constructor is unchanged. --- .../europa/ted/eforms/sdk/SdkConstants.java | 7 + .../sdk/repository/SdkNodeRepository.java | 69 +++++++++- .../sdk/repository/SdkNodeRepositoryTest.java | 123 ++++++++++++++++++ .../sdk-fixture/1.16/fields/fields.json | 16 +++ .../sdk-fixture/2.0/fields/fwd/nodes.json | 25 ++++ 5 files changed, 238 insertions(+), 2 deletions(-) create mode 100644 src/test/java/eu/europa/ted/eforms/sdk/repository/SdkNodeRepositoryTest.java create mode 100644 src/test/resources/sdk-fixture/1.16/fields/fields.json create mode 100644 src/test/resources/sdk-fixture/2.0/fields/fwd/nodes.json 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..f724caa 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"; 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..75d3a57 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 @@ -6,19 +6,84 @@ 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.SdkVersion; import eu.europa.ted.eforms.sdk.entity.SdkEntityFactory; import eu.europa.ted.eforms.sdk.entity.SdkNode; +import eu.europa.ted.eforms.sdk.resource.SdkResourceLoader; +/** + * Repository of the nodes of an SDK. + * + *

+ * SDK 1 publishes its nodes in the xmlStructure array of fields/fields.json. SDK 2 publishes them + * in the nodes array of fields/fwd/nodes.json, in the forward-compatible structure planned for + * SDK 3 (TEDEFO-5228). The nodes have the same properties in both, so the node entities are the + * same. This repository decides where the nodes of each SDK version come from, so that its callers + * do not have to check the SDK version. + *

+ */ public class SdkNodeRepository extends MapFromJson { private static final long serialVersionUID = 1L; + /** + * The major version of the only SDK that publishes no nodes of its own. + */ + private static final String SDK_MAJOR_WITHOUT_OWN_NODES = "1"; + + /** + * Creates the repository of the nodes of the given SDK, reading them from the file that this SDK + * version publishes them in. + * + * @param sdkVersion the target SDK version, which decides which file the nodes are read from + * @param sdkRootPath path of the root SDK folder + * @return the nodes of that SDK, by identifier + * @throws InstantiationException if a node cannot be created for this SDK version + */ + public static SdkNodeRepository forSdk(final String sdkVersion, final Path sdkRootPath) + throws InstantiationException { + if (SDK_MAJOR_WITHOUT_OWN_NODES.equals(new SdkVersion(sdkVersion).getMajor())) { + return new SdkNodeRepository(sdkVersion, + SdkResourceLoader.getResourceAsPath(sdkVersion, SdkConstants.SdkResource.FIELDS_JSON, + sdkRootPath), + SdkConstants.FIELDS_JSON_XML_STRUCTURE_KEY); + } + return new SdkNodeRepository(sdkVersion, + SdkResourceLoader.getResourceAsPath(sdkVersion, + SdkConstants.SdkResource.FIELDS_FWD_NODES_JSON, sdkRootPath), + SdkConstants.NODES_JSON_NODES_KEY); + } + + /** + * Creates the repository of the nodes from the xmlStructure array of the given fields.json. + * + * @deprecated Use {@link #forSdk(String, Path)}, which reads the nodes of SDK 2 from + * fields/fwd/nodes.json instead of fields/fields.json. + */ + @Deprecated public SdkNodeRepository(String sdkVersion, Path jsonPath) throws InstantiationException { - super(sdkVersion, jsonPath); + this(sdkVersion, jsonPath, SdkConstants.FIELDS_JSON_XML_STRUCTURE_KEY); + } + + private SdkNodeRepository(final String sdkVersion, final Path jsonPath, + final String nodesArrayKey) throws InstantiationException { + // The key travels as context: it must be known while the map is being populated, which + // happens during this call to super. + super(sdkVersion, jsonPath, nodesArrayKey); } @Override protected void populateMap(final JsonNode json) throws InstantiationException { - final ArrayNode nodes = (ArrayNode) json.get(SdkConstants.FIELDS_JSON_XML_STRUCTURE_KEY); + populateMap(json, SdkConstants.FIELDS_JSON_XML_STRUCTURE_KEY); + } + + @Override + protected void populateMap(final JsonNode json, final Object... context) + throws InstantiationException { + final String nodesArrayKey = context.length > 0 && context[0] instanceof String + ? (String) context[0] + : SdkConstants.FIELDS_JSON_XML_STRUCTURE_KEY; + + final ArrayNode nodes = (ArrayNode) json.get(nodesArrayKey); List needsParentWiring = new ArrayList<>(); // First pass: create all nodes, optimistically set parent if already loaded 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..3f013cc --- /dev/null +++ b/src/test/java/eu/europa/ted/eforms/sdk/repository/SdkNodeRepositoryTest.java @@ -0,0 +1,123 @@ +/* + * 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"; + + /** + * An SDK folder holding both files. 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 = SdkNodeRepository.forSdk(SDK_1, SDK_ROOT); + + 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 = SdkNodeRepository.forSdk(SDK_2, SDK_ROOT); + + 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 = SdkNodeRepository.forSdk(SDK_1, SDK_ROOT).get("ND-Lot"); + SdkNode ofSdk2 = SdkNodeRepository.forSdk(SDK_2, SDK_ROOT).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 = SdkNodeRepository.forSdk(SDK_1, SDK_ROOT); + SdkNodeRepository ofSdk2 = SdkNodeRepository.forSdk(SDK_2, SDK_ROOT); + + 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()); + } + + /** Each SDK version gets its own node implementation, as before. */ + @Test + void testEachSdkVersionGetsItsOwnNodes() throws InstantiationException { + SdkNode ofSdk1 = SdkNodeRepository.forSdk(SDK_1, SDK_ROOT).get("ND-Root"); + SdkNode ofSdk2 = SdkNodeRepository.forSdk(SDK_2, SDK_ROOT).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 deprecated constructor still reads the xmlStructure array of the file it is given. */ + @Test + @SuppressWarnings("deprecation") + void testTheDeprecatedConstructorStillReadsXmlStructure() 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..7503873 --- /dev/null +++ b/src/test/resources/sdk-fixture/1.16/fields/fields.json @@ -0,0 +1,16 @@ +{ + "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" : [ ] +} 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..69e2cf5 --- /dev/null +++ b/src/test/resources/sdk-fixture/2.0/fields/fwd/nodes.json @@ -0,0 +1,25 @@ +{ + "specialPurpose" : { + "ND-Fwd-Only" : "metadata" + }, + "nodes" : [ { + "id" : "ND-Root", + "xpathAbsolute" : "/*", + "xpathRelative" : "/*", + "repeatable" : false, + "alias" : "root" + }, { + "id" : "ND-Lot", + "parentId" : "ND-Root", + "xpathAbsolute" : "/*/cac:ProcurementProjectLot", + "xpathRelative" : "cac:ProcurementProjectLot", + "repeatable" : true, + "alias" : "lot" + }, { + "id" : "ND-Fwd-Only", + "parentId" : "ND-Lot", + "xpathAbsolute" : "/*/cac:ProcurementProjectLot/cac:FwdOnly", + "xpathRelative" : "cac:FwdOnly", + "repeatable" : false + } ] +} From 3f4f5dc5f808feec5b387150bd5fdf3092c43627 Mon Sep 17 00:00:00 2001 From: ROUSSY Christophe Date: Thu, 8 Oct 2026 11:46:57 +0200 Subject: [PATCH 03/13] TEDEFO-5229 Read the SDK 2 fields from fields/fwd/fields.json The fields of both SDK 1 and SDK 2 were read from fields/fields.json. SDK 2 publishes its fields in fields/fwd/fields.json, where some of the properties the ECL reads have a different structure, so for SDK 2 they are now read from there. SDK 1 keeps reading fields/fields.json. - Add SdkFieldRepository.forSdk(sdkVersion, sdkRootPath, nodes), which decides which file the fields come from, and links them to the nodes of the same SDK version (TEDEFO-5228). The array of fields has the same key in both files, so only the file differs. - Give SdkField an override seam for the properties whose structure differs: extractPrivacyCode, extractPrivacySettings, extractWithholdingCondition and extractUndisclosedFieldSelector, alongside the extractRepeatable that was already there. They default to what fields/fields.json gives, and are called while the field is being constructed, so an override reads the given node only. - Read the two EFX expressions of the fields that are withheld only for some of their instances, as they are and untranslated: SdkField.getWithholdingCondition() and getUndisclosedFieldSelector(). The EFX Toolkit needs them (TEDEFO-5129). - In SdkFieldV2, read the disclosureControl block: its groupId is the privacy code, and it carries the two expressions. The four fields that held the disclosure data are no longer given per field, so they are not read from it (TEDEFO-5230 links them instead). - SdkFieldV2 reads BOTH shapes, because an SDK 2 field is read from either file: fields/fields.json still gives a privacy block and an object-shaped repeatable, and that is what the EFX Toolkit still feeds it until TEDEFO-5232 moves it to forSdk. Each override takes the fwd shape when it is there and falls back to the inherited one otherwise. - Stop SdkFieldV2 inheriting the mapping of measure onto duration that SdkFieldV1 applies: SDK 1 had no duration type, SDK 2 separates the two (TEDEFO-4923), so an SDK 2 field typed measure reported duration. SdkField.getDeclaredType() gives a version the type the SDK declares, without the mapping. Reparenting SdkFieldV2 onto SdkField, which is where this belongs, is a binary incompatible change and has to wait for the major version. Tests: a new SdkFieldRepositoryTest over an SDK folder holding both files, which hold different fields on purpose, so the tests can tell which file was read. They cover both sources, repeatable in both shapes, the privacy code from groupId and from privacy.code, the two expressions present on a conditionally withheld field and absent otherwise, the disclosure fields still read for SDK 1 but not for SDK 2, the parent node linking, the measure/duration split per version, and the deprecated constructor unchanged. --- .../ted/eforms/sdk/entity/SdkField.java | 94 ++++++++- .../ted/eforms/sdk/entity/v2/SdkFieldV2.java | 87 ++++++++ .../sdk/repository/SdkFieldRepository.java | 41 ++++ .../repository/SdkFieldRepositoryTest.java | 186 ++++++++++++++++++ .../sdk-fixture/1.16/fields/fields.json | 49 ++++- .../sdk-fixture/2.0/fields/fwd/fields.json | 57 ++++++ 6 files changed, 510 insertions(+), 4 deletions(-) create mode 100644 src/test/java/eu/europa/ted/eforms/sdk/repository/SdkFieldRepositoryTest.java create mode 100644 src/test/resources/sdk-fixture/2.0/fields/fwd/fields.json 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..20a9cb9 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 @@ -18,6 +18,8 @@ public abstract class SdkField implements Comparable { private final boolean repeatable; private final String privacyCode; private final PrivacySettings privacySettings; + private final String withholdingCondition; + private final String undisclosedFieldSelector; private final List attributes; private final String attributeOf; private final String attributeName; @@ -119,6 +121,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 +136,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 = extractPrivacyCode(fieldNode); + this.privacySettings = extractPrivacySettings(fieldNode); + this.withholdingCondition = extractWithholdingCondition(fieldNode); + this.undisclosedFieldSelector = 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 +185,62 @@ protected boolean extractRepeatable(final JsonNode fieldNode) { return valueNode.asBoolean(false); } + /** + * Returns the privacy code of the field, from 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 extractPrivacyCode(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 extractPrivacySettings(final JsonNode fieldNode) { + return 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 +276,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 +345,24 @@ public PrivacySettings getPrivacySettings() { return this.privacySettings; } + /** + * @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..ef49ad0 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 privacy code as the groupId of the disclosureControl block, + * where fields/fields.json gives it as the code of the privacy block. + */ + @Override + protected String extractPrivacyCode(final JsonNode fieldNode) { + if (disclosureControlOf(fieldNode) == null) { + return super.extractPrivacyCode(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 extractPrivacySettings(final JsonNode fieldNode) { + if (disclosureControlOf(fieldNode) == null) { + return super.extractPrivacySettings(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 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/SdkFieldRepository.java b/src/main/java/eu/europa/ted/eforms/sdk/repository/SdkFieldRepository.java index a06f6aa..80763ee 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 @@ -6,16 +6,57 @@ 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.SdkVersion; import eu.europa.ted.eforms.sdk.entity.SdkEntityFactory; import eu.europa.ted.eforms.sdk.entity.SdkField; +import eu.europa.ted.eforms.sdk.resource.SdkResourceLoader; public class SdkFieldRepository extends MapFromJson { private static final long serialVersionUID = 1L; + /** + * The major version of the only SDK that publishes no fields of its own. + */ + private static final String SDK_MAJOR_WITHOUT_OWN_FIELDS = "1"; + + /** + * Creates the repository of the fields of the given SDK, reading them from the file that this SDK + * version publishes them in: fields/fields.json for SDK 1, fields/fwd/fields.json for SDK 2 + * (TEDEFO-5229). The array of fields has the same key in both. + * + * @param sdkVersion the target SDK version, which decides which file the fields are read from + * @param sdkRootPath path of the root SDK folder + * @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 a field cannot be created for this SDK version + */ + @SuppressWarnings("deprecation") + public static SdkFieldRepository forSdk(final String sdkVersion, final Path sdkRootPath, + final SdkNodeRepository nodeRepository) throws InstantiationException { + final SdkConstants.SdkResource fields = + SDK_MAJOR_WITHOUT_OWN_FIELDS.equals(new SdkVersion(sdkVersion).getMajor()) + ? SdkConstants.SdkResource.FIELDS_JSON + : SdkConstants.SdkResource.FIELDS_FWD_FIELDS_JSON; + + return new SdkFieldRepository(sdkVersion, + SdkResourceLoader.getResourceAsPath(sdkVersion, fields, sdkRootPath), nodeRepository); + } + + /** + * @deprecated Use {@link #forSdk(String, Path, SdkNodeRepository)}, which reads the fields of + * SDK 2 from fields/fwd/fields.json, and links them to their parent nodes. + */ + @Deprecated public SdkFieldRepository(String sdkVersion, Path jsonPath) throws InstantiationException { super(sdkVersion, jsonPath); } + /** + * @deprecated Use {@link #forSdk(String, Path, SdkNodeRepository)}, which reads the fields of + * SDK 2 from fields/fwd/fields.json. + */ + @Deprecated public SdkFieldRepository(String sdkVersion, Path jsonPath, SdkNodeRepository nodeRepository) throws InstantiationException { super(sdkVersion, jsonPath, nodeRepository); 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..4641c10 --- /dev/null +++ b/src/test/java/eu/europa/ted/eforms/sdk/repository/SdkFieldRepositoryTest.java @@ -0,0 +1,186 @@ +/* + * 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"; + + /** + * An SDK folder holding both files. 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 { + return SdkFieldRepository.forSdk(SDK_1, SDK_ROOT, SdkNodeRepository.forSdk(SDK_1, SDK_ROOT)); + } + + private static SdkFieldRepository ofSdk2() throws InstantiationException { + return SdkFieldRepository.forSdk(SDK_2, SDK_ROOT, SdkNodeRepository.forSdk(SDK_2, SDK_ROOT)); + } + + @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(6, 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 privacy code as the groupId of the disclosureControl block. */ + @Test + void testThePrivacyCodeComesFromGroupIdForSdk2() 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 2 no longer names the four disclosure fields per field: TEDEFO-5230 links them instead. For + * SDK 1 they are still read. + */ + @Test + void testTheDisclosureFieldsAreNoLongerGivenPerFieldForSdk2() throws InstantiationException { + SdkField.PrivacySettings ofSdk1 = ofSdk1().get("BT-Withheld").getPrivacySettings(); + + assertNotNull(ofSdk1, "SDK 1 still 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()); + + assertNull(ofSdk2().get("BT-Withheld").getPrivacySettings(), + "SDK 2 does not give them per field"); + } + + /** 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()); + } + + /** + * 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 deprecated constructor still reads the file it is given. */ + @Test + @SuppressWarnings("deprecation") + void testTheDeprecatedConstructorStillReadsTheFileItIsGiven() 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()); + } +} diff --git a/src/test/resources/sdk-fixture/1.16/fields/fields.json b/src/test/resources/sdk-fixture/1.16/fields/fields.json index 7503873..04c5904 100644 --- a/src/test/resources/sdk-fixture/1.16/fields/fields.json +++ b/src/test/resources/sdk-fixture/1.16/fields/fields.json @@ -12,5 +12,52 @@ "xpathRelative" : "cac:ProcurementProjectLot", "repeatable" : true } ], - "fields" : [ ] + "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/fwd/fields.json b/src/test/resources/sdk-fixture/2.0/fields/fwd/fields.json new file mode 100644 index 0000000..06eca4f --- /dev/null +++ b/src/test/resources/sdk-fixture/2.0/fields/fwd/fields.json @@ -0,0 +1,57 @@ +{ + "specialPurpose" : { + "BT-Fwd-Only" : "metadata" + }, + "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 + } ] +} From ca123c526c4fc8b42e2074d02b477ef6ee09cbeb Mon Sep 17 00:00:00 2001 From: ROUSSY Christophe Date: Thu, 8 Oct 2026 12:21:52 +0200 Subject: [PATCH 04/13] TEDEFO-5230 Load the SDK 2 special purpose maps and link the disclosure fields MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit fields/fwd/fields.json and fields/fwd/nodes.json each start with a specialPurpose map, which names the fields and nodes that have a special role by a key such as disclosureDate, so that applications use the key instead of hard-coding the identifier. Among them are the four disclosure fields. fields/fields.json names them in the privacy block of every withheld field; fields/fwd/fields.json no longer does, because they are the same for every field. The EFX Toolkit resolves the privacy properties of EFX 2 through the privacy settings of each field, so they have to stay available per field. - Read both maps in full, in the order of the file, and look them up by key: SdkFieldRepository.getSpecialPurposeFieldId(key) and SdkNodeRepository.getSpecialPurposeNodeId(key), with the whole map available too. An unknown key gives no result and is not an error, and so does an SDK version whose files carry no such map. - Give every field that can be withheld and does not name them itself the four disclosure fields of the map. The pass that resolves the identifiers to fields then runs as it does for SDK 1, so the Toolkit sees the same thing either way. They are read from the map only, with no fallback to a privacy block, as the ticket asks. - Each withheld field gets its own PrivacySettings: the identifiers are resolved to fields on them, so one shared instance would make the fields interfere. - SdkField.privacySettings is no longer final, with a setter, because SDK 2 fills them in after the field is read. - Add the keys to SdkConstants, and a package-private SpecialPurpose reader so both repositories parse the map the same way. Note the specialPurpose field of each repository has no initialiser on purpose: it is assigned while the map of entities is being populated, which happens during the call to super, and a field initialiser would run after that call and undo it. Tests: this reverses one assertion of TEDEFO-5229, deliberately — an SDK 2 withheld field had no privacy settings there, and now it has them from the map. The fixtures gain the two maps (the shape is key to identifier, which the placeholders had backwards) and the four notice fields, so the linking resolves end to end. New tests cover the lookup for fields and for nodes, an unknown key, an SDK 1 folder, the four identifiers and the fields they resolve to, a field that cannot be withheld, and that two withheld fields do not share their settings. --- .../europa/ted/eforms/sdk/SdkConstants.java | 33 ++++++++ .../ted/eforms/sdk/entity/SdkField.java | 14 +++- .../sdk/repository/SdkFieldRepository.java | 76 +++++++++++++++++ .../sdk/repository/SdkNodeRepository.java | 33 ++++++++ .../eforms/sdk/repository/SpecialPurpose.java | 59 +++++++++++++ .../repository/SdkFieldRepositoryTest.java | 82 +++++++++++++++++-- .../sdk/repository/SdkNodeRepositoryTest.java | 27 ++++++ .../sdk-fixture/2.0/fields/fwd/fields.json | 34 +++++++- .../sdk-fixture/2.0/fields/fwd/nodes.json | 3 +- 9 files changed, 351 insertions(+), 10 deletions(-) create mode 100644 src/main/java/eu/europa/ted/eforms/sdk/repository/SpecialPurpose.java 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 f724caa..71ec6ae 100644 --- a/src/main/java/eu/europa/ted/eforms/sdk/SdkConstants.java +++ b/src/main/java/eu/europa/ted/eforms/sdk/SdkConstants.java @@ -65,6 +65,39 @@ public class SdkConstants { */ public static final String FWD_DIR_NAME = "fwd"; + /** + * Key of the map that names the fields or the nodes that have a special role, in + * fields/fwd/fields.json and fields/fwd/nodes.json of SDK 2 (TEDEFO-1877, TEDEFO-5210). The keys + * of the map itself are plain strings, as the map is data and will grow (TEDEFO-5230). + */ + public static final String SPECIAL_PURPOSE_KEY = "specialPurpose"; + + /** + * Special purpose key of the field holding the disclosure group of a withheld field, which SDK 1 + * named per field as privacy.unpublishedFieldId. + */ + public static final String SPECIAL_PURPOSE_DISCLOSURE_GROUP = "disclosureGroup"; + + /** + * Special purpose key of the field holding the justification code of a withheld field, which + * SDK 1 named per field as privacy.reasonCodeFieldId. + */ + public static final String SPECIAL_PURPOSE_DISCLOSURE_JUSTIFICATION_CODE = + "disclosureJustificationCode"; + + /** + * Special purpose key of the field holding the justification description of a withheld field, + * which SDK 1 named per field as privacy.reasonDescriptionFieldId. + */ + public static final String SPECIAL_PURPOSE_DISCLOSURE_JUSTIFICATION_DESCRIPTION = + "disclosureJustificationDescription"; + + /** + * Special purpose key of the field holding the disclosure date of a withheld field, which SDK 1 + * named per field as privacy.publicationDateFieldId. + */ + public static final String SPECIAL_PURPOSE_DISCLOSURE_DATE = "disclosureDate"; + private SdkConstants() {} public enum SdkResource implements PathResource { 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 20a9cb9..d6beb9c 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,9 @@ 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 special purpose map of the SDK (TEDEFO-5230). + private PrivacySettings privacySettings; private final String withholdingCondition; private final String undisclosedFieldSelector; private final List attributes; @@ -345,6 +347,16 @@ 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, in the + * special purpose map of fields/fwd/fields.json, so they are filled in afterwards + * (TEDEFO-5230). + */ + public void setPrivacySettings(final PrivacySettings privacySettings) { + this.privacySettings = privacySettings; + } + /** * @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 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 80763ee..c163248 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,7 +2,9 @@ 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; @@ -19,6 +21,18 @@ public class SdkFieldRepository extends MapFromJson { */ private static final String SDK_MAJOR_WITHOUT_OWN_FIELDS = "1"; + /** + * The fields that have a special role, by their key (TEDEFO-5230). Empty 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 specialPurpose; + /** * Creates the repository of the fields of the given SDK, reading them from the file that this SDK * version publishes them in: fields/fields.json for SDK 1, fields/fwd/fields.json for SDK 2 @@ -74,6 +88,8 @@ protected void populateMap(final JsonNode json, final Object... context) ? (SdkNodeRepository) context[0] : null; + this.specialPurpose = SpecialPurpose.read(json); + final ArrayNode fields = (ArrayNode) json.get(SdkConstants.FIELDS_JSON_FIELDS_KEY); // First pass: create all field entities and add them to the map @@ -86,6 +102,11 @@ protected void populateMap(final JsonNode json, final Object... context) } } + // SDK 2 names the disclosure fields once, in the special purpose map, instead of naming them + // in the privacy block of every withheld field, so give them to each withheld field here. The + // pass below then resolves them to fields, as it does for SDK 1 (TEDEFO-5230). + linkTheDisclosureFields(); + // Second pass: resolve cross-field references for (final SdkField sdkField : this.values()) { if (sdkField.getPrivacySettings() != null) { @@ -122,4 +143,59 @@ protected void populateMap(final JsonNode json, final Object... context) } } } + + /** + * Gives the four disclosure fields of the special purpose map to every field that can be + * withheld and does not name them itself, so that the EFX Toolkit resolves the privacy + * properties of EFX 2 through the privacy settings of each field, as it does with + * fields/fields.json. + * + *

+ * A field that already names them keeps what it read: only SDK 2 stopped naming them per field. + * The disclosure fields are read from the map only, with no fallback to a privacy block. + *

+ */ + private void linkTheDisclosureFields() { + if (getSpecialPurpose().isEmpty()) { + return; + } + + final SdkField.PrivacySettings ofTheSdk = new SdkField.PrivacySettings( + getSpecialPurposeFieldId(SdkConstants.SPECIAL_PURPOSE_DISCLOSURE_GROUP), + getSpecialPurposeFieldId(SdkConstants.SPECIAL_PURPOSE_DISCLOSURE_JUSTIFICATION_CODE), + getSpecialPurposeFieldId( + SdkConstants.SPECIAL_PURPOSE_DISCLOSURE_JUSTIFICATION_DESCRIPTION), + getSpecialPurposeFieldId(SdkConstants.SPECIAL_PURPOSE_DISCLOSURE_DATE)); + + for (final SdkField sdkField : this.values()) { + // A field that can be withheld has a privacy code; one that already names the disclosure + // fields has its own settings, which must not be replaced. + if (sdkField.getPrivacyCode() != null && sdkField.getPrivacySettings() == null) { + // Each field gets its own settings: the pass below resolves the identifiers to fields on + // them, and they must not be shared between fields. + sdkField.setPrivacySettings(new SdkField.PrivacySettings( + ofTheSdk.getPrivacyCodeFieldId(), + ofTheSdk.getJustificationCodeFieldId(), + ofTheSdk.getJustificationDescriptionFieldId(), + ofTheSdk.getPublicationDateFieldId())); + } + } + } + + /** + * @return the fields that have a special role, by their key, as the SDK gives them. Empty for + * the SDK versions that publish no such map. + */ + public Map getSpecialPurpose() { + return specialPurpose == null ? Collections.emptyMap() : specialPurpose; + } + + /** + * @param key a special purpose key, such as "disclosureDate" + * @return the identifier of the field that has that role, or null if the SDK names none. An + * unknown key is not an error. + */ + public String getSpecialPurposeFieldId(final String key) { + return getSpecialPurpose().get(key); + } } 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 75d3a57..720aaaf 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,7 +2,9 @@ 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; @@ -30,6 +32,18 @@ public class SdkNodeRepository extends MapFromJson { */ private static final String SDK_MAJOR_WITHOUT_OWN_NODES = "1"; + /** + * The nodes that have a special role, by their key (TEDEFO-5230). Empty 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 specialPurpose; + /** * Creates the repository of the nodes of the given SDK, reading them from the file that this SDK * version publishes them in. @@ -83,6 +97,8 @@ protected void populateMap(final JsonNode json, final Object... context) ? (String) context[0] : SdkConstants.FIELDS_JSON_XML_STRUCTURE_KEY; + this.specialPurpose = SpecialPurpose.read(json); + final ArrayNode nodes = (ArrayNode) json.get(nodesArrayKey); List needsParentWiring = new ArrayList<>(); @@ -106,4 +122,21 @@ protected void populateMap(final JsonNode json, final Object... context) sdkNode.setParent(get(sdkNode.getParentId())); } } + + /** + * @return the nodes that have a special role, by their key, as the SDK gives them. Empty for the + * SDK versions that publish no such map. + */ + public Map getSpecialPurpose() { + return specialPurpose == null ? Collections.emptyMap() : specialPurpose; + } + + /** + * @param key a special purpose key, such as "root" + * @return the identifier of the node that has that role, or null if the SDK names none. An + * unknown key is not an error. + */ + public String getSpecialPurposeNodeId(final String key) { + return getSpecialPurpose().get(key); + } } diff --git a/src/main/java/eu/europa/ted/eforms/sdk/repository/SpecialPurpose.java b/src/main/java/eu/europa/ted/eforms/sdk/repository/SpecialPurpose.java new file mode 100644 index 0000000..57cf4a4 --- /dev/null +++ b/src/main/java/eu/europa/ted/eforms/sdk/repository/SpecialPurpose.java @@ -0,0 +1,59 @@ +/* + * 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.util.Collections; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.Map; +import com.fasterxml.jackson.databind.JsonNode; +import eu.europa.ted.eforms.sdk.SdkConstants; + +/** + * Reads the map that names the fields or the nodes that have a special role, which + * fields/fwd/fields.json and fields/fwd/nodes.json of SDK 2 each start with (TEDEFO-1877, + * TEDEFO-5210, TEDEFO-5230). + * + *

+ * Applications use the key, such as "disclosureDate", instead of hard-coding the identifier. The + * keys are plain strings, as the map is data and will grow. + *

+ */ +final class SpecialPurpose { + + private SpecialPurpose() {} + + /** + * @param json the root of a file that may start with a special purpose map + * @return the map as the file gives it, in the order of the file, or an empty map for the SDK + * versions whose files carry no such map + */ + static Map read(final JsonNode json) { + final JsonNode specialPurpose = json.get(SdkConstants.SPECIAL_PURPOSE_KEY); + if (specialPurpose == null || !specialPurpose.isObject()) { + return Collections.emptyMap(); + } + + // Keep the order of the file, so that what is read back is what the SDK gives. + final Map byKey = new LinkedHashMap<>(); + final Iterator keys = specialPurpose.fieldNames(); + while (keys.hasNext()) { + final String key = keys.next(); + if (specialPurpose.hasNonNull(key)) { + byKey.put(key, specialPurpose.get(key).asText(null)); + } + } + return Collections.unmodifiableMap(byKey); + } +} 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 index 4641c10..e708f27 100644 --- a/src/test/java/eu/europa/ted/eforms/sdk/repository/SdkFieldRepositoryTest.java +++ b/src/test/java/eu/europa/ted/eforms/sdk/repository/SdkFieldRepositoryTest.java @@ -16,6 +16,7 @@ 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.assertNotSame; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; import java.nio.file.Path; @@ -71,7 +72,7 @@ void testSdk1ReadsTheFieldsFromFieldsJson() throws InstantiationException { void testSdk2ReadsTheFieldsFromTheFwdFile() throws InstantiationException { SdkFieldRepository fields = ofSdk2(); - assertEquals(6, fields.size()); + assertEquals(10, fields.size()); assertNotNull(fields.get(FIELD_OF_THE_FWD_FILE), "the fields of SDK 2 must come from fields/fwd/fields.json"); } @@ -126,21 +127,88 @@ void testTheFieldsWithoutExpressions() throws InstantiationException { } /** - * SDK 2 no longer names the four disclosure fields per field: TEDEFO-5230 links them instead. For - * SDK 1 they are still read. + * SDK 1 names the four disclosure fields in the privacy block of every withheld field. */ @Test - void testTheDisclosureFieldsAreNoLongerGivenPerFieldForSdk2() throws InstantiationException { + void testSdk1NamesTheDisclosureFieldsPerField() throws InstantiationException { SdkField.PrivacySettings ofSdk1 = ofSdk1().get("BT-Withheld").getPrivacySettings(); - assertNotNull(ofSdk1, "SDK 1 still names them in the privacy block"); + 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, in the special purpose map, 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 testSdk2TakesTheDisclosureFieldsFromTheSpecialPurposeMap() 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"); + } + + /** + * Each withheld field gets its own settings, because the identifiers are resolved to fields on + * them: sharing one instance between fields would make them interfere. + */ + @Test + void testEachWithheldFieldHasItsOwnSettings() throws InstantiationException { + SdkFieldRepository ofSdk2 = ofSdk2(); + + assertNotSame(ofSdk2.get("BT-Withheld").getPrivacySettings(), + ofSdk2.get("BT-WithheldSometimes").getPrivacySettings()); + } + + /** The special purpose map of the fields is read in full, and looked up by key. */ + @Test + void testTheSpecialPurposeFieldsAreLoaded() throws InstantiationException { + SdkFieldRepository ofSdk2 = ofSdk2(); + + assertEquals(5, ofSdk2.getSpecialPurpose().size()); + assertEquals("BT-198-notice", ofSdk2.getSpecialPurposeFieldId("disclosureDate")); + assertEquals("OPP-070-notice", ofSdk2.getSpecialPurposeFieldId("noticeSubType")); + } + + /** An unknown key gives no result, and is not an error. */ + @Test + void testAnUnknownSpecialPurposeKeyGivesNothing() throws InstantiationException { + assertNull(ofSdk2().getSpecialPurposeFieldId("thereIsNoSuchKey")); + } + + /** SDK 1 has no such map, so the lookup gives no result. */ + @Test + void testSdk1HasNoSpecialPurposeFields() throws InstantiationException { + SdkFieldRepository ofSdk1 = ofSdk1(); - assertNull(ofSdk2().get("BT-Withheld").getPrivacySettings(), - "SDK 2 does not give them per field"); + assertTrue(ofSdk1.getSpecialPurpose().isEmpty()); + assertNull(ofSdk1.getSpecialPurposeFieldId("disclosureDate")); } /** The fields are linked to the nodes loaded from the file of the same SDK version. */ 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 index 3f013cc..506aa44 100644 --- a/src/test/java/eu/europa/ted/eforms/sdk/repository/SdkNodeRepositoryTest.java +++ b/src/test/java/eu/europa/ted/eforms/sdk/repository/SdkNodeRepositoryTest.java @@ -99,6 +99,33 @@ void testTheParentsAreWiredForBothVersions() throws InstantiationException { ofSdk2.get(NODE_OF_THE_FWD_FILE).getAncestry()); } + /** The special purpose map of the nodes is read in full, and looked up by key (TEDEFO-5230). */ + @Test + void testTheSpecialPurposeNodesAreLoaded() throws InstantiationException { + SdkNodeRepository ofSdk2 = SdkNodeRepository.forSdk(SDK_2, SDK_ROOT); + + assertEquals(2, ofSdk2.getSpecialPurpose().size()); + assertEquals("ND-Root", ofSdk2.getSpecialPurposeNodeId("root")); + assertEquals("ND-Organizations", ofSdk2.getSpecialPurposeNodeId("organizations")); + assertNotNull(ofSdk2.get(ofSdk2.getSpecialPurposeNodeId("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 testAnUnknownSpecialPurposeKeyGivesNothing() throws InstantiationException { + assertNull(SdkNodeRepository.forSdk(SDK_2, SDK_ROOT).getSpecialPurposeNodeId("thereIsNoSuchKey")); + } + + /** SDK 1 has no such map, so the lookup gives no result. */ + @Test + void testSdk1HasNoSpecialPurposeNodes() throws InstantiationException { + SdkNodeRepository ofSdk1 = SdkNodeRepository.forSdk(SDK_1, SDK_ROOT); + + assertTrue(ofSdk1.getSpecialPurpose().isEmpty()); + assertNull(ofSdk1.getSpecialPurposeNodeId("root")); + } + /** Each SDK version gets its own node implementation, as before. */ @Test void testEachSdkVersionGetsItsOwnNodes() throws InstantiationException { 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 index 06eca4f..ba4b425 100644 --- a/src/test/resources/sdk-fixture/2.0/fields/fwd/fields.json +++ b/src/test/resources/sdk-fixture/2.0/fields/fwd/fields.json @@ -1,6 +1,10 @@ { "specialPurpose" : { - "BT-Fwd-Only" : "metadata" + "disclosureDate" : "BT-198-notice", + "disclosureGroup" : "BT-195-notice", + "disclosureJustificationCode" : "BT-197-notice", + "disclosureJustificationDescription" : "BT-196-notice", + "noticeSubType" : "OPP-070-notice" }, "fields" : [ { "id" : "BT-Plain", @@ -53,5 +57,33 @@ "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 index 69e2cf5..cf39e1c 100644 --- a/src/test/resources/sdk-fixture/2.0/fields/fwd/nodes.json +++ b/src/test/resources/sdk-fixture/2.0/fields/fwd/nodes.json @@ -1,6 +1,7 @@ { "specialPurpose" : { - "ND-Fwd-Only" : "metadata" + "root" : "ND-Root", + "organizations" : "ND-Organizations" }, "nodes" : [ { "id" : "ND-Root", From bf1f0b03648239401a2aef29be8e25fa3cb6b881 Mon Sep 17 00:00:00 2001 From: Ioannis Rousochatzakis Date: Sun, 11 Oct 2026 23:26:23 +0200 Subject: [PATCH 05/13] TEDEFO-5231: Read the data types without a version --- .../sdk/component/SdkComponentType.java | 2 +- .../ted/eforms/sdk/entity/SdkDataType.java | 13 +--- .../eforms/sdk/entity/SdkEntityFactory.java | 6 -- .../eforms/sdk/entity/v1/SdkDataTypeV1.java | 35 ----------- .../eforms/sdk/entity/v2/SdkDataTypeV2.java | 40 ------------ .../sdk/repository/SdkDataTypeRepository.java | 63 ++++++------------- .../repository/SdkDataTypeRepositoryTest.java | 25 +++----- 7 files changed, 30 insertions(+), 154 deletions(-) delete mode 100644 src/main/java/eu/europa/ted/eforms/sdk/entity/v1/SdkDataTypeV1.java delete mode 100644 src/main/java/eu/europa/ted/eforms/sdk/entity/v2/SdkDataTypeV2.java 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 d6f9a08..a81fbce 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, DATA_TYPE, EFX_EXPRESSION_TRANSLATOR, EFX_TEMPLATE_TRANSLATOR, EFX_RULES_TRANSLATOR, + FIELD, NODE, CODELIST, NOTICE_TYPE, 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 5a9a2a2..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,16 +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. - * - *

- * 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 (TEDEFO-5231). The - * version-specific implementations are - * {@link eu.europa.ted.eforms.sdk.entity.v1.SdkDataTypeV1} and - * {@link eu.europa.ted.eforms.sdk.entity.v2.SdkDataTypeV2}, selected by - * {@link SdkEntityFactory#getSdkDataType(String, JsonNode)}. - *

+ * 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/SdkEntityFactory.java b/src/main/java/eu/europa/ted/eforms/sdk/entity/SdkEntityFactory.java index 6612a13..09705d9 100644 --- a/src/main/java/eu/europa/ted/eforms/sdk/entity/SdkEntityFactory.java +++ b/src/main/java/eu/europa/ted/eforms/sdk/entity/SdkEntityFactory.java @@ -36,10 +36,4 @@ public static SdkNoticeSubtype getSdkNoticeSubtype(final String sdkVersion, fina return SdkEntityFactory.INSTANCE.getComponentImpl(sdkVersion, SdkComponentType.NOTICE_TYPE, SdkNoticeSubtype.class, json); } - - public static SdkDataType getSdkDataType(final String sdkVersion, final JsonNode json) - throws InstantiationException { - return SdkEntityFactory.INSTANCE.getComponentImpl(sdkVersion, SdkComponentType.DATA_TYPE, - SdkDataType.class, json); - } } diff --git a/src/main/java/eu/europa/ted/eforms/sdk/entity/v1/SdkDataTypeV1.java b/src/main/java/eu/europa/ted/eforms/sdk/entity/v1/SdkDataTypeV1.java deleted file mode 100644 index 535fce4..0000000 --- a/src/main/java/eu/europa/ted/eforms/sdk/entity/v1/SdkDataTypeV1.java +++ /dev/null @@ -1,35 +0,0 @@ -/* - * 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.entity.v1; - -import com.fasterxml.jackson.databind.JsonNode; -import eu.europa.ted.eforms.sdk.component.SdkComponent; -import eu.europa.ted.eforms.sdk.component.SdkComponentType; -import eu.europa.ted.eforms.sdk.entity.SdkDataType; - -/** - * An SDK 1 data type. SDK 1 publishes no data types of its own, so these are read from the - * data-types.json resource of this library (TEDEFO-5231). - */ -@SdkComponent(versions = {"1"}, componentType = SdkComponentType.DATA_TYPE) -public class SdkDataTypeV1 extends SdkDataType { - - public SdkDataTypeV1(final String id, final String privacyMask) { - super(id, privacyMask); - } - - public SdkDataTypeV1(final JsonNode dataType) { - super(dataType); - } -} diff --git a/src/main/java/eu/europa/ted/eforms/sdk/entity/v2/SdkDataTypeV2.java b/src/main/java/eu/europa/ted/eforms/sdk/entity/v2/SdkDataTypeV2.java deleted file mode 100644 index 984b046..0000000 --- a/src/main/java/eu/europa/ted/eforms/sdk/entity/v2/SdkDataTypeV2.java +++ /dev/null @@ -1,40 +0,0 @@ -/* - * 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.entity.v2; - -import com.fasterxml.jackson.databind.JsonNode; -import eu.europa.ted.eforms.sdk.component.SdkComponent; -import eu.europa.ted.eforms.sdk.component.SdkComponentType; -import eu.europa.ted.eforms.sdk.entity.v1.SdkDataTypeV1; - -/** - * An SDK 2 data type, read from fields/fwd/data-types.json of the SDK (TEDEFO-5231). - * - *

- * The file has the same format as the SDK 1 resource for the properties this entity reads, so - * there is nothing to add here yet. The class exists so that the two versions can diverge without - * changing the callers, as for the other entities that differ per version. - *

- */ -@SdkComponent(versions = {"2"}, componentType = SdkComponentType.DATA_TYPE) -public class SdkDataTypeV2 extends SdkDataTypeV1 { - - public SdkDataTypeV2(final String id, final String privacyMask) { - super(id, privacyMask); - } - - public SdkDataTypeV2(final JsonNode dataType) { - super(dataType); - } -} 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 7c41303..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 @@ -23,61 +23,39 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import eu.europa.ted.eforms.sdk.SdkConstants; -import eu.europa.ted.eforms.sdk.SdkVersion; import eu.europa.ted.eforms.sdk.entity.SdkDataType; -import eu.europa.ted.eforms.sdk.entity.SdkEntityFactory; -import eu.europa.ted.eforms.sdk.resource.SdkResourceLoader; /** * Repository of SDK data types. * *

- * SDK 1 publishes no data types, so they are read from the data-types.json resource of this - * library. SDK 2 publishes them in fields/fwd/data-types.json, which has the same format, so for - * SDK 2 they are read from the SDK itself (TEDEFO-5231). This repository decides where the data - * types of each SDK version come from, so that its callers do not have to check the SDK version. + * 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; /** - * The major version of the only SDK that publishes no data types of its own. - */ - private static final String SDK_MAJOR_WITHOUT_DATA_TYPES = "1"; - - /** - * Creates the repository of the data types of the given SDK, with their privacy masks, the - * attributes that their fields carry, and the code lists of their values. + * 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 sdkVersion the target SDK version, which decides where the data types are read from - * @param sdkRootPath path of the root SDK folder, used for the SDK versions that publish their - * data types; may be null for SDK 1, which does not - * @throws InstantiationException if a data type cannot be created for this SDK version + * @param sdkPath the folder of the SDK */ - public SdkDataTypeRepository(final String sdkVersion, final Path sdkRootPath) - throws InstantiationException { - // The file of the SDK has the same format as the resource, so the loop is the same for both. - for (final JsonNode dataType : readDataTypes(sdkVersion, sdkRootPath) - .get(SdkConstants.DATA_TYPES_JSON_DATA_TYPES_KEY)) { - final SdkDataType sdkDataType = SdkEntityFactory.getSdkDataType(sdkVersion, dataType); - this.put(sdkDataType.getId(), sdkDataType); - } + 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, - * whatever the SDK version. - * - * @deprecated Use {@link #SdkDataTypeRepository(String, Path)}, which reads the data types of - * SDK 2 from the SDK instead of this copy. + * Creates the repository of the data types from the data-types.json resource of this library. */ - @Deprecated 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) { @@ -122,16 +100,11 @@ private void addType(String id, String privacyMask) { this.put(id, new SdkDataType(id, privacyMask)); } - /** - * @return the data types of the given SDK version, from the SDK itself when it publishes them, - * and from the resource of this library for the SDK versions that do not - */ - private static JsonNode readDataTypes(final String sdkVersion, final Path sdkRootPath) { - if (SDK_MAJOR_WITHOUT_DATA_TYPES.equals(new SdkVersion(sdkVersion).getMajor())) { - return readResource(); + 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); } - return readSdkFile(SdkResourceLoader.getResourceAsPath(sdkVersion, - SdkConstants.SdkResource.FIELDS_FWD_DATA_TYPES, sdkRootPath)); } private static JsonNode readResource() { 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 336a2fc..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 @@ -20,8 +20,6 @@ import java.util.Set; import org.junit.jupiter.api.Test; import eu.europa.ted.eforms.sdk.entity.SdkDataType; -import eu.europa.ted.eforms.sdk.entity.v1.SdkDataTypeV1; -import eu.europa.ted.eforms.sdk.entity.v2.SdkDataTypeV2; class SdkDataTypeRepositoryTest { @@ -31,15 +29,18 @@ class SdkDataTypeRepositoryTest { /** SDK 2 publishes its data types in fields/fwd/data-types.json. */ private static final String SDK_2 = "2.0"; - /** An SDK folder holding only the data types of SDK 2, deliberately different from the resource. */ + /** + * 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_1, null); + return new SdkDataTypeRepository(SDK_ROOT.resolve(SDK_1)); } private static SdkDataTypeRepository ofSdk2() throws InstantiationException { - return new SdkDataTypeRepository(SDK_2, SDK_ROOT); + return new SdkDataTypeRepository(SDK_ROOT.resolve(SDK_2)); } @Test @@ -143,15 +144,6 @@ void testSdk2IgnoresThePropertiesThatADataTypeDoesNotRead() throws Instantiation assertNull(date.getListName()); } - /** Each SDK version gets its own data type implementation, as the other entities do. */ - @Test - void testEachSdkVersionGetsItsOwnDataTypes() throws InstantiationException { - final SdkDataType ofSdk1 = ofSdk1().get("amount"); - final SdkDataType ofSdk2 = ofSdk2().get("amount"); - - assertTrue(ofSdk1 instanceof SdkDataTypeV1, "SDK 1 gives " + ofSdk1.getClass()); - assertTrue(ofSdk2 instanceof SdkDataTypeV2, "SDK 2 gives " + ofSdk2.getClass()); - } @Test @SuppressWarnings("deprecation") @@ -170,10 +162,9 @@ void testCreateDefault_HasTheOriginalTypesWithTheirMasksOnly() throws Instantiat } } - /** The deprecated constructor still reads the resource, whatever the SDK version. */ + /** The constructor without arguments reads the resource of this library. */ @Test - @SuppressWarnings("deprecation") - void testTheDeprecatedConstructorStillReadsTheResource() { + void testTheConstructorWithoutArgumentsReadsTheResource() { assertEquals(23, new SdkDataTypeRepository().size()); } } From 8ac7eeb6a104412fbc0c8a3149e4f9fa9d6fca47 Mon Sep 17 00:00:00 2001 From: Ioannis Rousochatzakis Date: Sun, 11 Oct 2026 23:27:45 +0200 Subject: [PATCH 06/13] TEDEFO-5228: Read the nodes of each SDK version with its own repository --- .../sdk/component/SdkComponentType.java | 2 +- .../sdk/repository/SdkNodeRepository.java | 95 +++++-------------- .../sdk/repository/SdkRepositoryFactory.java | 43 +++++++++ .../repository/v1/SdkNodeRepositoryV1.java | 37 ++++++++ .../repository/v2/SdkNodeRepositoryV2.java | 46 +++++++++ .../repository/SdkFieldRepositoryTest.java | 6 +- .../sdk/repository/SdkNodeRepositoryTest.java | 33 +++---- 7 files changed, 174 insertions(+), 88 deletions(-) create mode 100644 src/main/java/eu/europa/ted/eforms/sdk/repository/SdkRepositoryFactory.java create mode 100644 src/main/java/eu/europa/ted/eforms/sdk/repository/v1/SdkNodeRepositoryV1.java create mode 100644 src/main/java/eu/europa/ted/eforms/sdk/repository/v2/SdkNodeRepositoryV2.java 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..38c3b17 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, 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/repository/SdkNodeRepository.java b/src/main/java/eu/europa/ted/eforms/sdk/repository/SdkNodeRepository.java index 720aaaf..e808a27 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 @@ -8,33 +8,23 @@ 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.SdkVersion; import eu.europa.ted.eforms.sdk.entity.SdkEntityFactory; import eu.europa.ted.eforms.sdk.entity.SdkNode; -import eu.europa.ted.eforms.sdk.resource.SdkResourceLoader; /** - * Repository of the nodes of an SDK. + * The nodes of an SDK, by identifier. * *

- * SDK 1 publishes its nodes in the xmlStructure array of fields/fields.json. SDK 2 publishes them - * in the nodes array of fields/fwd/nodes.json, in the forward-compatible structure planned for - * SDK 3 (TEDEFO-5228). The nodes have the same properties in both, so the node entities are the - * same. This repository decides where the nodes of each SDK version come from, so that its callers - * do not have to check the SDK version. + * 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 major version of the only SDK that publishes no nodes of its own. - */ - private static final String SDK_MAJOR_WITHOUT_OWN_NODES = "1"; - - /** - * The nodes that have a special role, by their key (TEDEFO-5230). Empty for the SDK versions - * that publish no such map. + * The nodes that have a special role, by their key (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 @@ -44,71 +34,31 @@ public class SdkNodeRepository extends MapFromJson { */ private Map specialPurpose; - /** - * Creates the repository of the nodes of the given SDK, reading them from the file that this SDK - * version publishes them in. - * - * @param sdkVersion the target SDK version, which decides which file the nodes are read from - * @param sdkRootPath path of the root SDK folder - * @return the nodes of that SDK, by identifier - * @throws InstantiationException if a node cannot be created for this SDK version - */ - public static SdkNodeRepository forSdk(final String sdkVersion, final Path sdkRootPath) - throws InstantiationException { - if (SDK_MAJOR_WITHOUT_OWN_NODES.equals(new SdkVersion(sdkVersion).getMajor())) { - return new SdkNodeRepository(sdkVersion, - SdkResourceLoader.getResourceAsPath(sdkVersion, SdkConstants.SdkResource.FIELDS_JSON, - sdkRootPath), - SdkConstants.FIELDS_JSON_XML_STRUCTURE_KEY); - } - return new SdkNodeRepository(sdkVersion, - SdkResourceLoader.getResourceAsPath(sdkVersion, - SdkConstants.SdkResource.FIELDS_FWD_NODES_JSON, sdkRootPath), - SdkConstants.NODES_JSON_NODES_KEY); - } - /** * Creates the repository of the nodes from the xmlStructure array of the given fields.json. - * - * @deprecated Use {@link #forSdk(String, Path)}, which reads the nodes of SDK 2 from - * fields/fwd/nodes.json instead of fields/fields.json. */ - @Deprecated public SdkNodeRepository(String sdkVersion, Path jsonPath) throws InstantiationException { - this(sdkVersion, jsonPath, SdkConstants.FIELDS_JSON_XML_STRUCTURE_KEY); - } - - private SdkNodeRepository(final String sdkVersion, final Path jsonPath, - final String nodesArrayKey) throws InstantiationException { - // The key travels as context: it must be known while the map is being populated, which - // happens during this call to super. - super(sdkVersion, jsonPath, nodesArrayKey); + super(sdkVersion, jsonPath); } @Override protected void populateMap(final JsonNode json) throws InstantiationException { - populateMap(json, SdkConstants.FIELDS_JSON_XML_STRUCTURE_KEY); + this.addNodes((ArrayNode) json.get(SdkConstants.FIELDS_JSON_XML_STRUCTURE_KEY)); } - @Override - protected void populateMap(final JsonNode json, final Object... context) - throws InstantiationException { - final String nodesArrayKey = context.length > 0 && context[0] instanceof String - ? (String) context[0] - : SdkConstants.FIELDS_JSON_XML_STRUCTURE_KEY; - - this.specialPurpose = SpecialPurpose.read(json); - - final ArrayNode nodes = (ArrayNode) json.get(nodesArrayKey); - List needsParentWiring = new ArrayList<>(); + /** + * 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 { @@ -118,17 +68,24 @@ protected void populateMap(final JsonNode json, final Object... context) } // 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 the nodes that have a special role, which the given file starts with. + */ + protected void readSpecialPurpose(final JsonNode json) { + this.specialPurpose = SpecialPurpose.read(json); + } + /** * @return the nodes that have a special role, by their key, as the SDK gives them. Empty for the * SDK versions that publish no such map. */ public Map getSpecialPurpose() { - return specialPurpose == null ? Collections.emptyMap() : specialPurpose; + return this.specialPurpose == null ? Collections.emptyMap() : this.specialPurpose; } /** @@ -137,6 +94,6 @@ public Map getSpecialPurpose() { * unknown key is not an error. */ public String getSpecialPurposeNodeId(final String key) { - return getSpecialPurpose().get(key); + return this.getSpecialPurpose().get(key); } } 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..e83a05c --- /dev/null +++ b/src/main/java/eu/europa/ted/eforms/sdk/repository/SdkRepositoryFactory.java @@ -0,0 +1,43 @@ +/* + * 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); + } +} 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/SdkNodeRepositoryV2.java b/src/main/java/eu/europa/ted/eforms/sdk/repository/v2/SdkNodeRepositoryV2.java new file mode 100644 index 0000000..d9e2178 --- /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 the + * nodes that have a special role, 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.readSpecialPurpose(json); + this.addNodes((ArrayNode) json.get(SdkConstants.NODES_JSON_NODES_KEY)); + } +} 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 index e708f27..526a951 100644 --- a/src/test/java/eu/europa/ted/eforms/sdk/repository/SdkFieldRepositoryTest.java +++ b/src/test/java/eu/europa/ted/eforms/sdk/repository/SdkFieldRepositoryTest.java @@ -48,11 +48,13 @@ class SdkFieldRepositoryTest { private static final String FIELD_OF_THE_FWD_FILE = "BT-Fwd-Only"; private static SdkFieldRepository ofSdk1() throws InstantiationException { - return SdkFieldRepository.forSdk(SDK_1, SDK_ROOT, SdkNodeRepository.forSdk(SDK_1, SDK_ROOT)); + return SdkFieldRepository.forSdk(SDK_1, SDK_ROOT, + SdkRepositoryFactory.getSdkNodeRepository(SDK_1, SDK_ROOT.resolve(SDK_1))); } private static SdkFieldRepository ofSdk2() throws InstantiationException { - return SdkFieldRepository.forSdk(SDK_2, SDK_ROOT, SdkNodeRepository.forSdk(SDK_2, SDK_ROOT)); + return SdkFieldRepository.forSdk(SDK_2, SDK_ROOT, + SdkRepositoryFactory.getSdkNodeRepository(SDK_2, SDK_ROOT.resolve(SDK_2))); } @Test 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 index 506aa44..7a3101e 100644 --- a/src/test/java/eu/europa/ted/eforms/sdk/repository/SdkNodeRepositoryTest.java +++ b/src/test/java/eu/europa/ted/eforms/sdk/repository/SdkNodeRepositoryTest.java @@ -37,8 +37,8 @@ class SdkNodeRepositoryTest { private static final String SDK_2 = "2.0"; /** - * An SDK folder holding both files. The two hold different nodes on purpose, so that a test can - * tell which one was read. + * 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"); @@ -47,7 +47,7 @@ class SdkNodeRepositoryTest { @Test void testSdk1ReadsTheNodesFromFieldsJson() throws InstantiationException { - SdkNodeRepository nodes = SdkNodeRepository.forSdk(SDK_1, SDK_ROOT); + SdkNodeRepository nodes = SdkRepositoryFactory.getSdkNodeRepository(SDK_1, SDK_ROOT.resolve(SDK_1)); assertEquals(2, nodes.size()); assertEquals("/*/cac:ProcurementProjectLot", nodes.get("ND-Lot").getXpathAbsolute()); @@ -61,7 +61,7 @@ void testSdk1ReadsTheNodesFromFieldsJson() throws InstantiationException { */ @Test void testSdk2ReadsTheNodesFromTheFwdFile() throws InstantiationException { - SdkNodeRepository nodes = SdkNodeRepository.forSdk(SDK_2, SDK_ROOT); + SdkNodeRepository nodes = SdkRepositoryFactory.getSdkNodeRepository(SDK_2, SDK_ROOT.resolve(SDK_2)); assertEquals(3, nodes.size()); assertNotNull(nodes.get(NODE_OF_THE_FWD_FILE), @@ -73,8 +73,8 @@ void testSdk2ReadsTheNodesFromTheFwdFile() throws InstantiationException { /** The nodes have the same properties in both files, so the node entities do not change. */ @Test void testTheNodePropertiesAreTheSameInBothFiles() throws InstantiationException { - SdkNode ofSdk1 = SdkNodeRepository.forSdk(SDK_1, SDK_ROOT).get("ND-Lot"); - SdkNode ofSdk2 = SdkNodeRepository.forSdk(SDK_2, SDK_ROOT).get("ND-Lot"); + 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()); @@ -87,8 +87,8 @@ void testTheNodePropertiesAreTheSameInBothFiles() throws InstantiationException /** The parents are wired whichever file the nodes come from, so the ancestry resolves. */ @Test void testTheParentsAreWiredForBothVersions() throws InstantiationException { - SdkNodeRepository ofSdk1 = SdkNodeRepository.forSdk(SDK_1, SDK_ROOT); - SdkNodeRepository ofSdk2 = SdkNodeRepository.forSdk(SDK_2, SDK_ROOT); + 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"); @@ -102,7 +102,7 @@ void testTheParentsAreWiredForBothVersions() throws InstantiationException { /** The special purpose map of the nodes is read in full, and looked up by key (TEDEFO-5230). */ @Test void testTheSpecialPurposeNodesAreLoaded() throws InstantiationException { - SdkNodeRepository ofSdk2 = SdkNodeRepository.forSdk(SDK_2, SDK_ROOT); + SdkNodeRepository ofSdk2 = SdkRepositoryFactory.getSdkNodeRepository(SDK_2, SDK_ROOT.resolve(SDK_2)); assertEquals(2, ofSdk2.getSpecialPurpose().size()); assertEquals("ND-Root", ofSdk2.getSpecialPurposeNodeId("root")); @@ -114,13 +114,13 @@ void testTheSpecialPurposeNodesAreLoaded() throws InstantiationException { /** An unknown key gives no result, and is not an error. */ @Test void testAnUnknownSpecialPurposeKeyGivesNothing() throws InstantiationException { - assertNull(SdkNodeRepository.forSdk(SDK_2, SDK_ROOT).getSpecialPurposeNodeId("thereIsNoSuchKey")); + assertNull(SdkRepositoryFactory.getSdkNodeRepository(SDK_2, SDK_ROOT.resolve(SDK_2)).getSpecialPurposeNodeId("thereIsNoSuchKey")); } /** SDK 1 has no such map, so the lookup gives no result. */ @Test void testSdk1HasNoSpecialPurposeNodes() throws InstantiationException { - SdkNodeRepository ofSdk1 = SdkNodeRepository.forSdk(SDK_1, SDK_ROOT); + SdkNodeRepository ofSdk1 = SdkRepositoryFactory.getSdkNodeRepository(SDK_1, SDK_ROOT.resolve(SDK_1)); assertTrue(ofSdk1.getSpecialPurpose().isEmpty()); assertNull(ofSdk1.getSpecialPurposeNodeId("root")); @@ -129,18 +129,19 @@ void testSdk1HasNoSpecialPurposeNodes() throws InstantiationException { /** Each SDK version gets its own node implementation, as before. */ @Test void testEachSdkVersionGetsItsOwnNodes() throws InstantiationException { - SdkNode ofSdk1 = SdkNodeRepository.forSdk(SDK_1, SDK_ROOT).get("ND-Root"); - SdkNode ofSdk2 = SdkNodeRepository.forSdk(SDK_2, SDK_ROOT).get("ND-Root"); + 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 deprecated constructor still reads the xmlStructure array of the file it is given. */ + /** + * The constructor that takes a fields.json reads the xmlStructure array of the file it is given. + */ @Test - @SuppressWarnings("deprecation") - void testTheDeprecatedConstructorStillReadsXmlStructure() throws InstantiationException { + void testTheConstructorReadsTheXmlStructureOfTheFileItIsGiven() throws InstantiationException { SdkNodeRepository nodes = new SdkNodeRepository(SDK_1, SDK_ROOT.resolve(Path.of("1.16", "fields", "fields.json"))); From 2cd7073f1f80e347fc27ed3d85353734ec2d2a1c Mon Sep 17 00:00:00 2001 From: Ioannis Rousochatzakis Date: Sun, 11 Oct 2026 23:49:58 +0200 Subject: [PATCH 07/13] TEDEFO-5229: Read the fields of each SDK version with its own repository --- .../sdk/component/SdkComponentType.java | 2 +- .../ted/eforms/sdk/entity/SdkField.java | 10 +- .../ted/eforms/sdk/entity/v2/SdkFieldV2.java | 2 +- .../sdk/repository/SdkFieldRepository.java | 144 ++++++------------ .../sdk/repository/SdkRepositoryFactory.java | 15 ++ .../repository/v1/SdkFieldRepositoryV1.java | 41 +++++ .../repository/v2/SdkFieldRepositoryV2.java | 97 ++++++++++++ .../repository/SdkFieldRepositoryTest.java | 60 ++++++-- .../sdk-fixture/2.0/fields/fields.json | 93 +++++++++++ 9 files changed, 349 insertions(+), 115 deletions(-) create mode 100644 src/main/java/eu/europa/ted/eforms/sdk/repository/v1/SdkFieldRepositoryV1.java create mode 100644 src/main/java/eu/europa/ted/eforms/sdk/repository/v2/SdkFieldRepositoryV2.java create mode 100644 src/test/resources/sdk-fixture/2.0/fields/fields.json 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 38c3b17..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, NODE_REPOSITORY, 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/SdkField.java b/src/main/java/eu/europa/ted/eforms/sdk/entity/SdkField.java index d6beb9c..ee9d196 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 @@ -138,10 +138,10 @@ protected SdkField(final JsonNode fieldNode) { this.type = fieldNode.get("type").asText(null); this.codelistId = extractCodelistId(fieldNode); this.repeatable = extractRepeatable(fieldNode); - this.privacyCode = extractPrivacyCode(fieldNode); - this.privacySettings = extractPrivacySettings(fieldNode); - this.withholdingCondition = extractWithholdingCondition(fieldNode); - this.undisclosedFieldSelector = extractUndisclosedFieldSelector(fieldNode); + this.privacyCode = this.extractPrivacyCode(fieldNode); + this.privacySettings = this.extractPrivacySettings(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; @@ -212,7 +212,7 @@ protected String extractPrivacyCode(final JsonNode fieldNode) { *

*/ protected PrivacySettings extractPrivacySettings(final JsonNode fieldNode) { - return extractPrivacy(fieldNode.get("privacy")); + return this.extractPrivacy(fieldNode.get("privacy")); } /** 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 ef49ad0..ba5689d 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 @@ -113,7 +113,7 @@ protected String extractUndisclosedFieldSelector(final JsonNode fieldNode) { */ @Override public String getType() { - return getDeclaredType(); + return this.getDeclaredType(); } private static JsonNode disclosureControlOf(final JsonNode fieldNode) { 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 c163248..06250bd 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 @@ -8,21 +8,23 @@ 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.SdkVersion; import eu.europa.ted.eforms.sdk.entity.SdkEntityFactory; import eu.europa.ted.eforms.sdk.entity.SdkField; -import eu.europa.ted.eforms.sdk.resource.SdkResourceLoader; +/** + * 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 major version of the only SDK that publishes no fields of its own. - */ - private static final String SDK_MAJOR_WITHOUT_OWN_FIELDS = "1"; - - /** - * The fields that have a special role, by their key (TEDEFO-5230). Empty for the SDK versions + * The fields that have a special role, by their key (TEDEFO-5230). Null for the SDK versions * that publish no such map. * *

@@ -33,44 +35,10 @@ public class SdkFieldRepository extends MapFromJson { */ private Map specialPurpose; - /** - * Creates the repository of the fields of the given SDK, reading them from the file that this SDK - * version publishes them in: fields/fields.json for SDK 1, fields/fwd/fields.json for SDK 2 - * (TEDEFO-5229). The array of fields has the same key in both. - * - * @param sdkVersion the target SDK version, which decides which file the fields are read from - * @param sdkRootPath path of the root SDK folder - * @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 a field cannot be created for this SDK version - */ - @SuppressWarnings("deprecation") - public static SdkFieldRepository forSdk(final String sdkVersion, final Path sdkRootPath, - final SdkNodeRepository nodeRepository) throws InstantiationException { - final SdkConstants.SdkResource fields = - SDK_MAJOR_WITHOUT_OWN_FIELDS.equals(new SdkVersion(sdkVersion).getMajor()) - ? SdkConstants.SdkResource.FIELDS_JSON - : SdkConstants.SdkResource.FIELDS_FWD_FIELDS_JSON; - - return new SdkFieldRepository(sdkVersion, - SdkResourceLoader.getResourceAsPath(sdkVersion, fields, sdkRootPath), nodeRepository); - } - - /** - * @deprecated Use {@link #forSdk(String, Path, SdkNodeRepository)}, which reads the fields of - * SDK 2 from fields/fwd/fields.json, and links them to their parent nodes. - */ - @Deprecated public SdkFieldRepository(String sdkVersion, Path jsonPath) throws InstantiationException { super(sdkVersion, jsonPath); } - /** - * @deprecated Use {@link #forSdk(String, Path, SdkNodeRepository)}, which reads the fields of - * SDK 2 from fields/fwd/fields.json. - */ - @Deprecated public SdkFieldRepository(String sdkVersion, Path jsonPath, SdkNodeRepository nodeRepository) throws InstantiationException { super(sdkVersion, jsonPath, nodeRepository); @@ -78,40 +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; + } - this.specialPurpose = SpecialPurpose.read(json); - - 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())); } } + } - // SDK 2 names the disclosure fields once, in the special purpose map, instead of naming them - // in the privacy block of every withheld field, so give them to each withheld field here. The - // pass below then resolves them to fields, as it does for SDK 1 (TEDEFO-5230). - linkTheDisclosureFields(); - - // 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())); } @@ -128,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); } @@ -145,41 +122,10 @@ protected void populateMap(final JsonNode json, final Object... context) } /** - * Gives the four disclosure fields of the special purpose map to every field that can be - * withheld and does not name them itself, so that the EFX Toolkit resolves the privacy - * properties of EFX 2 through the privacy settings of each field, as it does with - * fields/fields.json. - * - *

- * A field that already names them keeps what it read: only SDK 2 stopped naming them per field. - * The disclosure fields are read from the map only, with no fallback to a privacy block. - *

+ * Reads the map of the fields that have a special role, which the given file starts with. */ - private void linkTheDisclosureFields() { - if (getSpecialPurpose().isEmpty()) { - return; - } - - final SdkField.PrivacySettings ofTheSdk = new SdkField.PrivacySettings( - getSpecialPurposeFieldId(SdkConstants.SPECIAL_PURPOSE_DISCLOSURE_GROUP), - getSpecialPurposeFieldId(SdkConstants.SPECIAL_PURPOSE_DISCLOSURE_JUSTIFICATION_CODE), - getSpecialPurposeFieldId( - SdkConstants.SPECIAL_PURPOSE_DISCLOSURE_JUSTIFICATION_DESCRIPTION), - getSpecialPurposeFieldId(SdkConstants.SPECIAL_PURPOSE_DISCLOSURE_DATE)); - - for (final SdkField sdkField : this.values()) { - // A field that can be withheld has a privacy code; one that already names the disclosure - // fields has its own settings, which must not be replaced. - if (sdkField.getPrivacyCode() != null && sdkField.getPrivacySettings() == null) { - // Each field gets its own settings: the pass below resolves the identifiers to fields on - // them, and they must not be shared between fields. - sdkField.setPrivacySettings(new SdkField.PrivacySettings( - ofTheSdk.getPrivacyCodeFieldId(), - ofTheSdk.getJustificationCodeFieldId(), - ofTheSdk.getJustificationDescriptionFieldId(), - ofTheSdk.getPublicationDateFieldId())); - } - } + protected void readSpecialPurpose(final JsonNode json) { + this.specialPurpose = SpecialPurpose.read(json); } /** @@ -187,7 +133,7 @@ private void linkTheDisclosureFields() { * the SDK versions that publish no such map. */ public Map getSpecialPurpose() { - return specialPurpose == null ? Collections.emptyMap() : specialPurpose; + return this.specialPurpose == null ? Collections.emptyMap() : this.specialPurpose; } /** @@ -196,6 +142,6 @@ public Map getSpecialPurpose() { * unknown key is not an error. */ public String getSpecialPurposeFieldId(final String key) { - return getSpecialPurpose().get(key); + return this.getSpecialPurpose().get(key); } } 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 index e83a05c..b841d1c 100644 --- a/src/main/java/eu/europa/ted/eforms/sdk/repository/SdkRepositoryFactory.java +++ b/src/main/java/eu/europa/ted/eforms/sdk/repository/SdkRepositoryFactory.java @@ -40,4 +40,19 @@ public static SdkNodeRepository getSdkNodeRepository(final String sdkVersion, 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/v2/SdkFieldRepositoryV2.java b/src/main/java/eu/europa/ted/eforms/sdk/repository/v2/SdkFieldRepositoryV2.java new file mode 100644 index 0000000..495fabe --- /dev/null +++ b/src/main/java/eu/europa/ted/eforms/sdk/repository/v2/SdkFieldRepositoryV2.java @@ -0,0 +1,97 @@ +/* + * 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 the + * fields that have a special role, which the file starts with (TEDEFO-5230). + * + *

+ * SDK 2 names the four disclosure fields once, in the special purpose map, 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.readSpecialPurpose(json); + this.addFields((ArrayNode) json.get(SdkConstants.FIELDS_JSON_FIELDS_KEY), + nodeRepositoryOf(context)); + this.linkTheDisclosureFields(); + this.resolveReferences(); + } + + /** + * Gives the four disclosure fields of the special purpose map to every field that can be + * withheld and does not name them itself, so that the EFX Toolkit resolves the privacy + * properties of EFX 2 through the privacy settings of each field, as it does with + * fields/fields.json. + * + *

+ * A field that already names them keeps what it read: only SDK 2 stopped naming them per field. + * The disclosure fields are read from the map only, with no fallback to a privacy block. + *

+ */ + private void linkTheDisclosureFields() { + if (this.getSpecialPurpose().isEmpty()) { + return; + } + final SdkField.PrivacySettings ofTheSdk = new SdkField.PrivacySettings( + this.getSpecialPurposeFieldId(SdkConstants.SPECIAL_PURPOSE_DISCLOSURE_GROUP), + this.getSpecialPurposeFieldId(SdkConstants.SPECIAL_PURPOSE_DISCLOSURE_JUSTIFICATION_CODE), + this.getSpecialPurposeFieldId( + SdkConstants.SPECIAL_PURPOSE_DISCLOSURE_JUSTIFICATION_DESCRIPTION), + this.getSpecialPurposeFieldId(SdkConstants.SPECIAL_PURPOSE_DISCLOSURE_DATE)); + for (final SdkField sdkField : this.values()) { + // A field that can be withheld has a privacy code; one that already names the disclosure + // fields has its own settings, which must not be replaced. + if (sdkField.getPrivacyCode() != null && sdkField.getPrivacySettings() == null) { + // Each field gets its own settings: the pass below resolves the identifiers to fields on + // them, and they must not be shared between fields. + sdkField.setPrivacySettings(new SdkField.PrivacySettings( + ofTheSdk.getPrivacyCodeFieldId(), + ofTheSdk.getJustificationCodeFieldId(), + ofTheSdk.getJustificationDescriptionFieldId(), + ofTheSdk.getPublicationDateFieldId())); + } + } + } +} 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 index 526a951..13ea5be 100644 --- a/src/test/java/eu/europa/ted/eforms/sdk/repository/SdkFieldRepositoryTest.java +++ b/src/test/java/eu/europa/ted/eforms/sdk/repository/SdkFieldRepositoryTest.java @@ -39,8 +39,8 @@ class SdkFieldRepositoryTest { private static final String SDK_2 = "2.0"; /** - * An SDK folder holding both files. The two hold different fields on purpose, so that a test can - * tell which one was read. + * 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"); @@ -48,13 +48,15 @@ class SdkFieldRepositoryTest { private static final String FIELD_OF_THE_FWD_FILE = "BT-Fwd-Only"; private static SdkFieldRepository ofSdk1() throws InstantiationException { - return SdkFieldRepository.forSdk(SDK_1, SDK_ROOT, - SdkRepositoryFactory.getSdkNodeRepository(SDK_1, SDK_ROOT.resolve(SDK_1))); + 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 { - return SdkFieldRepository.forSdk(SDK_2, SDK_ROOT, - SdkRepositoryFactory.getSdkNodeRepository(SDK_2, SDK_ROOT.resolve(SDK_2))); + final Path sdk = SDK_ROOT.resolve(SDK_2); + return SdkRepositoryFactory.getSdkFieldRepository(SDK_2, sdk, + SdkRepositoryFactory.getSdkNodeRepository(SDK_2, sdk)); } @Test @@ -220,6 +222,23 @@ void testTheFieldsAreLinkedToTheirParentNode() throws InstantiationException { 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. @@ -243,14 +262,37 @@ void testEachSdkVersionGetsItsOwnFields() throws InstantiationException { assertEquals("plain", ((SdkFieldV2) ofSdk2).getAlias(), "SDK 2 also reads the alias"); } - /** The deprecated constructor still reads the file it is given. */ + /** The constructor that takes a fields.json reads the file it is given. */ @Test - @SuppressWarnings("deprecation") - void testTheDeprecatedConstructorStillReadsTheFileItIsGiven() throws InstantiationException { + 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/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" + } + } ] +} From e90f1f18d4970d19478db74b4bd6a789f3fb44eb Mon Sep 17 00:00:00 2001 From: Ioannis Rousochatzakis Date: Mon, 12 Oct 2026 00:32:35 +0200 Subject: [PATCH 08/13] TEDEFO-5230: Attach the disclosure fields to each field that can be withheld --- .../repository/v2/SdkFieldRepositoryV2.java | 34 ++++++------------- .../repository/SdkFieldRepositoryTest.java | 13 ------- 2 files changed, 10 insertions(+), 37 deletions(-) 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 index 495fabe..cc7df13 100644 --- 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 @@ -60,37 +60,23 @@ protected void populateMap(final JsonNode json, final Object... context) } /** - * Gives the four disclosure fields of the special purpose map to every field that can be - * withheld and does not name them itself, so that the EFX Toolkit resolves the privacy - * properties of EFX 2 through the privacy settings of each field, as it does with - * fields/fields.json. - * - *

- * A field that already names them keeps what it read: only SDK 2 stopped naming them per field. - * The disclosure fields are read from the map only, with no fallback to a privacy block. - *

+ * Gives the four disclosure fields of the special purpose map 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 only, with no fallback to the privacy blocks of fields/fields.json, so a file + * without the map gives no disclosure fields. */ private void linkTheDisclosureFields() { if (this.getSpecialPurpose().isEmpty()) { return; } - final SdkField.PrivacySettings ofTheSdk = new SdkField.PrivacySettings( - this.getSpecialPurposeFieldId(SdkConstants.SPECIAL_PURPOSE_DISCLOSURE_GROUP), - this.getSpecialPurposeFieldId(SdkConstants.SPECIAL_PURPOSE_DISCLOSURE_JUSTIFICATION_CODE), - this.getSpecialPurposeFieldId( - SdkConstants.SPECIAL_PURPOSE_DISCLOSURE_JUSTIFICATION_DESCRIPTION), - this.getSpecialPurposeFieldId(SdkConstants.SPECIAL_PURPOSE_DISCLOSURE_DATE)); for (final SdkField sdkField : this.values()) { - // A field that can be withheld has a privacy code; one that already names the disclosure - // fields has its own settings, which must not be replaced. - if (sdkField.getPrivacyCode() != null && sdkField.getPrivacySettings() == null) { - // Each field gets its own settings: the pass below resolves the identifiers to fields on - // them, and they must not be shared between fields. + if (sdkField.getPrivacyCode() != null) { sdkField.setPrivacySettings(new SdkField.PrivacySettings( - ofTheSdk.getPrivacyCodeFieldId(), - ofTheSdk.getJustificationCodeFieldId(), - ofTheSdk.getJustificationDescriptionFieldId(), - ofTheSdk.getPublicationDateFieldId())); + this.getSpecialPurposeFieldId(SdkConstants.SPECIAL_PURPOSE_DISCLOSURE_GROUP), + this.getSpecialPurposeFieldId(SdkConstants.SPECIAL_PURPOSE_DISCLOSURE_JUSTIFICATION_CODE), + this.getSpecialPurposeFieldId( + SdkConstants.SPECIAL_PURPOSE_DISCLOSURE_JUSTIFICATION_DESCRIPTION), + this.getSpecialPurposeFieldId(SdkConstants.SPECIAL_PURPOSE_DISCLOSURE_DATE))); } } } 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 index 13ea5be..a8a00d3 100644 --- a/src/test/java/eu/europa/ted/eforms/sdk/repository/SdkFieldRepositoryTest.java +++ b/src/test/java/eu/europa/ted/eforms/sdk/repository/SdkFieldRepositoryTest.java @@ -16,7 +16,6 @@ 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.assertNotSame; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; import java.nio.file.Path; @@ -178,18 +177,6 @@ void testAFieldThatCannotBeWithheldHasNoDisclosureFields() throws InstantiationE "a field that is not withheld has no disclosure fields"); } - /** - * Each withheld field gets its own settings, because the identifiers are resolved to fields on - * them: sharing one instance between fields would make them interfere. - */ - @Test - void testEachWithheldFieldHasItsOwnSettings() throws InstantiationException { - SdkFieldRepository ofSdk2 = ofSdk2(); - - assertNotSame(ofSdk2.get("BT-Withheld").getPrivacySettings(), - ofSdk2.get("BT-WithheldSometimes").getPrivacySettings()); - } - /** The special purpose map of the fields is read in full, and looked up by key. */ @Test void testTheSpecialPurposeFieldsAreLoaded() throws InstantiationException { From 42242c5db4e9b54ff172d491daaf9f34e2a6bef4 Mon Sep 17 00:00:00 2001 From: Ioannis Rousochatzakis Date: Mon, 12 Oct 2026 10:40:41 +0200 Subject: [PATCH 09/13] TEDEFO-5230: Read the special purpose maps as stable identifiers --- .../europa/ted/eforms/sdk/SdkConstants.java | 31 ++++++---- .../ted/eforms/sdk/entity/SdkField.java | 7 +-- .../eforms/sdk/repository/MapFromJson.java | 36 +++++++++++ .../sdk/repository/SdkFieldRepository.java | 30 +++++----- .../sdk/repository/SdkNodeRepository.java | 30 +++++----- .../eforms/sdk/repository/SpecialPurpose.java | 59 ------------------- .../repository/v2/SdkFieldRepositoryV2.java | 28 ++++----- .../repository/v2/SdkNodeRepositoryV2.java | 6 +- .../repository/SdkFieldRepositoryTest.java | 24 ++++---- .../sdk/repository/SdkNodeRepositoryTest.java | 22 +++---- 10 files changed, 128 insertions(+), 145 deletions(-) delete mode 100644 src/main/java/eu/europa/ted/eforms/sdk/repository/SpecialPurpose.java 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 71ec6ae..da21331 100644 --- a/src/main/java/eu/europa/ted/eforms/sdk/SdkConstants.java +++ b/src/main/java/eu/europa/ted/eforms/sdk/SdkConstants.java @@ -66,37 +66,44 @@ public class SdkConstants { public static final String FWD_DIR_NAME = "fwd"; /** - * Key of the map that names the fields or the nodes that have a special role, in - * fields/fwd/fields.json and fields/fwd/nodes.json of SDK 2 (TEDEFO-1877, TEDEFO-5210). The keys - * of the map itself are plain strings, as the map is data and will grow (TEDEFO-5230). + * 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 SPECIAL_PURPOSE_KEY = "specialPurpose"; + public static final String STABLE_IDS_KEY = "specialPurpose"; /** - * Special purpose key of the field holding the disclosure group of a withheld field, which SDK 1 + * 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 SPECIAL_PURPOSE_DISCLOSURE_GROUP = "disclosureGroup"; + public static final String STABLE_ID_DISCLOSURE_GROUP = "disclosureGroup"; /** - * Special purpose key of the field holding the justification code of a withheld field, which + * 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 SPECIAL_PURPOSE_DISCLOSURE_JUSTIFICATION_CODE = + public static final String STABLE_ID_DISCLOSURE_JUSTIFICATION_CODE = "disclosureJustificationCode"; /** - * Special purpose key of the field holding the justification description of a withheld field, + * 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 SPECIAL_PURPOSE_DISCLOSURE_JUSTIFICATION_DESCRIPTION = + public static final String STABLE_ID_DISCLOSURE_JUSTIFICATION_DESCRIPTION = "disclosureJustificationDescription"; /** - * Special purpose key of the field holding the disclosure date of a withheld field, which SDK 1 + * 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 SPECIAL_PURPOSE_DISCLOSURE_DATE = "disclosureDate"; + public static final String STABLE_ID_DISCLOSURE_DATE = "disclosureDate"; private SdkConstants() {} 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 ee9d196..81dd387 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 @@ -18,7 +18,7 @@ public abstract class SdkField implements Comparable { private final boolean repeatable; private final String privacyCode; // Not final: SDK 2 no longer names the disclosure fields per field, so they are filled in after - // this field is read, from the special purpose map of the SDK (TEDEFO-5230). + // this field is read, from the stable identifiers of the SDK (TEDEFO-5230). private PrivacySettings privacySettings; private final String withholdingCondition; private final String undisclosedFieldSelector; @@ -349,9 +349,8 @@ public PrivacySettings getPrivacySettings() { /** * 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, in the - * special purpose map of fields/fwd/fields.json, so they are filled in afterwards - * (TEDEFO-5230). + * 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 setPrivacySettings(final PrivacySettings privacySettings) { this.privacySettings = privacySettings; 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/SdkFieldRepository.java b/src/main/java/eu/europa/ted/eforms/sdk/repository/SdkFieldRepository.java index 06250bd..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 @@ -24,8 +24,8 @@ public class SdkFieldRepository extends MapFromJson { private static final long serialVersionUID = 1L; /** - * The fields that have a special role, by their key (TEDEFO-5230). Null for the SDK versions - * that publish no such map. + * 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 @@ -33,7 +33,7 @@ public class SdkFieldRepository extends MapFromJson { * that call and undo it. *

*/ - private Map specialPurpose; + private Map stableIds; public SdkFieldRepository(String sdkVersion, Path jsonPath) throws InstantiationException { super(sdkVersion, jsonPath); @@ -122,26 +122,26 @@ protected void resolveReferences() { } /** - * Reads the map of the fields that have a special role, which the given file starts with. + * Reads the map of stable identifiers, which the given file starts with. */ - protected void readSpecialPurpose(final JsonNode json) { - this.specialPurpose = SpecialPurpose.read(json); + protected void readStableIds(final JsonNode json) { + this.stableIds = stableIdsOf(json); } /** - * @return the fields that have a special role, by their key, as the SDK gives them. Empty for - * the SDK versions that publish no such map. + * @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 getSpecialPurpose() { - return this.specialPurpose == null ? Collections.emptyMap() : this.specialPurpose; + public Map getStableIds() { + return this.stableIds == null ? Collections.emptyMap() : this.stableIds; } /** - * @param key a special purpose key, such as "disclosureDate" - * @return the identifier of the field that has that role, or null if the SDK names none. An - * unknown key is not an error. + * @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 getSpecialPurposeFieldId(final String key) { - return this.getSpecialPurpose().get(key); + 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 e808a27..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 @@ -23,8 +23,8 @@ public class SdkNodeRepository extends MapFromJson { private static final long serialVersionUID = 1L; /** - * The nodes that have a special role, by their key (TEDEFO-5230). Null for the SDK versions that - * publish no such map. + * 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 @@ -32,7 +32,7 @@ public class SdkNodeRepository extends MapFromJson { * that call and undo it. *

*/ - private Map specialPurpose; + private Map stableIds; /** * Creates the repository of the nodes from the xmlStructure array of the given fields.json. @@ -74,26 +74,26 @@ protected void addNodes(final ArrayNode nodes) throws InstantiationException { } /** - * Reads the map of the nodes that have a special role, which the given file starts with. + * Reads the map of stable identifiers, which the given file starts with. */ - protected void readSpecialPurpose(final JsonNode json) { - this.specialPurpose = SpecialPurpose.read(json); + protected void readStableIds(final JsonNode json) { + this.stableIds = stableIdsOf(json); } /** - * @return the nodes that have a special role, by their key, as the SDK gives them. Empty for the - * SDK versions that publish no such map. + * @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 getSpecialPurpose() { - return this.specialPurpose == null ? Collections.emptyMap() : this.specialPurpose; + public Map getStableIds() { + return this.stableIds == null ? Collections.emptyMap() : this.stableIds; } /** - * @param key a special purpose key, such as "root" - * @return the identifier of the node that has that role, or null if the SDK names none. An - * unknown key is not an error. + * @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 getSpecialPurposeNodeId(final String key) { - return this.getSpecialPurpose().get(key); + public String getNodeIdByStableId(final String stableId) { + return this.getStableIds().get(stableId); } } diff --git a/src/main/java/eu/europa/ted/eforms/sdk/repository/SpecialPurpose.java b/src/main/java/eu/europa/ted/eforms/sdk/repository/SpecialPurpose.java deleted file mode 100644 index 57cf4a4..0000000 --- a/src/main/java/eu/europa/ted/eforms/sdk/repository/SpecialPurpose.java +++ /dev/null @@ -1,59 +0,0 @@ -/* - * 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.util.Collections; -import java.util.Iterator; -import java.util.LinkedHashMap; -import java.util.Map; -import com.fasterxml.jackson.databind.JsonNode; -import eu.europa.ted.eforms.sdk.SdkConstants; - -/** - * Reads the map that names the fields or the nodes that have a special role, which - * fields/fwd/fields.json and fields/fwd/nodes.json of SDK 2 each start with (TEDEFO-1877, - * TEDEFO-5210, TEDEFO-5230). - * - *

- * Applications use the key, such as "disclosureDate", instead of hard-coding the identifier. The - * keys are plain strings, as the map is data and will grow. - *

- */ -final class SpecialPurpose { - - private SpecialPurpose() {} - - /** - * @param json the root of a file that may start with a special purpose map - * @return the map as the file gives it, in the order of the file, or an empty map for the SDK - * versions whose files carry no such map - */ - static Map read(final JsonNode json) { - final JsonNode specialPurpose = json.get(SdkConstants.SPECIAL_PURPOSE_KEY); - if (specialPurpose == null || !specialPurpose.isObject()) { - return Collections.emptyMap(); - } - - // Keep the order of the file, so that what is read back is what the SDK gives. - final Map byKey = new LinkedHashMap<>(); - final Iterator keys = specialPurpose.fieldNames(); - while (keys.hasNext()) { - final String key = keys.next(); - if (specialPurpose.hasNonNull(key)) { - byKey.put(key, specialPurpose.get(key).asText(null)); - } - } - return Collections.unmodifiableMap(byKey); - } -} 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 index cc7df13..4f4eaac 100644 --- 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 @@ -24,11 +24,11 @@ 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 the - * fields that have a special role, which the file starts with (TEDEFO-5230). + * 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, in the special purpose map, instead of naming them + * 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. *

@@ -52,7 +52,7 @@ public SdkFieldRepositoryV2(final String sdkVersion, final Path sdkPath, @Override protected void populateMap(final JsonNode json, final Object... context) throws InstantiationException { - this.readSpecialPurpose(json); + this.readStableIds(json); this.addFields((ArrayNode) json.get(SdkConstants.FIELDS_JSON_FIELDS_KEY), nodeRepositoryOf(context)); this.linkTheDisclosureFields(); @@ -60,23 +60,23 @@ protected void populateMap(final JsonNode json, final Object... context) } /** - * Gives the four disclosure fields of the special purpose map 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 only, with no fallback to the privacy blocks of fields/fields.json, so a file - * without the map gives no disclosure fields. + * 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.getSpecialPurpose().isEmpty()) { + if (this.getStableIds().isEmpty()) { return; } for (final SdkField sdkField : this.values()) { if (sdkField.getPrivacyCode() != null) { sdkField.setPrivacySettings(new SdkField.PrivacySettings( - this.getSpecialPurposeFieldId(SdkConstants.SPECIAL_PURPOSE_DISCLOSURE_GROUP), - this.getSpecialPurposeFieldId(SdkConstants.SPECIAL_PURPOSE_DISCLOSURE_JUSTIFICATION_CODE), - this.getSpecialPurposeFieldId( - SdkConstants.SPECIAL_PURPOSE_DISCLOSURE_JUSTIFICATION_DESCRIPTION), - this.getSpecialPurposeFieldId(SdkConstants.SPECIAL_PURPOSE_DISCLOSURE_DATE))); + 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 index d9e2178..82a30c7 100644 --- 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 @@ -22,8 +22,8 @@ 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 the - * nodes that have a special role, which the file starts with (TEDEFO-5230). + * 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 { @@ -40,7 +40,7 @@ public SdkNodeRepositoryV2(final String sdkVersion, final Path sdkPath) @Override protected void populateMap(final JsonNode json) throws InstantiationException { - this.readSpecialPurpose(json); + 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/SdkFieldRepositoryTest.java b/src/test/java/eu/europa/ted/eforms/sdk/repository/SdkFieldRepositoryTest.java index a8a00d3..2db2ab5 100644 --- a/src/test/java/eu/europa/ted/eforms/sdk/repository/SdkFieldRepositoryTest.java +++ b/src/test/java/eu/europa/ted/eforms/sdk/repository/SdkFieldRepositoryTest.java @@ -144,12 +144,12 @@ void testSdk1NamesTheDisclosureFieldsPerField() throws InstantiationException { } /** - * SDK 2 names them once, in the special purpose map, so every field that can be withheld gets + * 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 testSdk2TakesTheDisclosureFieldsFromTheSpecialPurposeMap() throws InstantiationException { + void testSdk2TakesTheDisclosureFieldsFromTheStableIds() throws InstantiationException { SdkField.PrivacySettings ofSdk2 = ofSdk2().get("BT-Withheld").getPrivacySettings(); assertNotNull(ofSdk2, "a withheld field must still have its disclosure fields"); @@ -177,29 +177,29 @@ void testAFieldThatCannotBeWithheldHasNoDisclosureFields() throws InstantiationE "a field that is not withheld has no disclosure fields"); } - /** The special purpose map of the fields is read in full, and looked up by key. */ + /** The stable identifiers of the fields are read in full, and each gives its field. */ @Test - void testTheSpecialPurposeFieldsAreLoaded() throws InstantiationException { + void testTheStableIdsOfTheFieldsAreLoaded() throws InstantiationException { SdkFieldRepository ofSdk2 = ofSdk2(); - assertEquals(5, ofSdk2.getSpecialPurpose().size()); - assertEquals("BT-198-notice", ofSdk2.getSpecialPurposeFieldId("disclosureDate")); - assertEquals("OPP-070-notice", ofSdk2.getSpecialPurposeFieldId("noticeSubType")); + 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 testAnUnknownSpecialPurposeKeyGivesNothing() throws InstantiationException { - assertNull(ofSdk2().getSpecialPurposeFieldId("thereIsNoSuchKey")); + void testAnUnknownStableIdGivesNothing() throws InstantiationException { + assertNull(ofSdk2().getFieldIdByStableId("thereIsNoSuchKey")); } /** SDK 1 has no such map, so the lookup gives no result. */ @Test - void testSdk1HasNoSpecialPurposeFields() throws InstantiationException { + void testSdk1HasNoStableIds() throws InstantiationException { SdkFieldRepository ofSdk1 = ofSdk1(); - assertTrue(ofSdk1.getSpecialPurpose().isEmpty()); - assertNull(ofSdk1.getSpecialPurposeFieldId("disclosureDate")); + assertTrue(ofSdk1.getStableIds().isEmpty()); + assertNull(ofSdk1.getFieldIdByStableId("disclosureDate")); } /** The fields are linked to the nodes loaded from the file of the same SDK version. */ 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 index 7a3101e..c80147b 100644 --- a/src/test/java/eu/europa/ted/eforms/sdk/repository/SdkNodeRepositoryTest.java +++ b/src/test/java/eu/europa/ted/eforms/sdk/repository/SdkNodeRepositoryTest.java @@ -99,31 +99,31 @@ void testTheParentsAreWiredForBothVersions() throws InstantiationException { ofSdk2.get(NODE_OF_THE_FWD_FILE).getAncestry()); } - /** The special purpose map of the nodes is read in full, and looked up by key (TEDEFO-5230). */ + /** The stable identifiers of the nodes are read in full, and each gives its node. */ @Test - void testTheSpecialPurposeNodesAreLoaded() throws InstantiationException { + void testTheStableIdsOfTheNodesAreLoaded() throws InstantiationException { SdkNodeRepository ofSdk2 = SdkRepositoryFactory.getSdkNodeRepository(SDK_2, SDK_ROOT.resolve(SDK_2)); - assertEquals(2, ofSdk2.getSpecialPurpose().size()); - assertEquals("ND-Root", ofSdk2.getSpecialPurposeNodeId("root")); - assertEquals("ND-Organizations", ofSdk2.getSpecialPurposeNodeId("organizations")); - assertNotNull(ofSdk2.get(ofSdk2.getSpecialPurposeNodeId("root")), + 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 testAnUnknownSpecialPurposeKeyGivesNothing() throws InstantiationException { - assertNull(SdkRepositoryFactory.getSdkNodeRepository(SDK_2, SDK_ROOT.resolve(SDK_2)).getSpecialPurposeNodeId("thereIsNoSuchKey")); + 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 testSdk1HasNoSpecialPurposeNodes() throws InstantiationException { + void testSdk1HasNoStableIds() throws InstantiationException { SdkNodeRepository ofSdk1 = SdkRepositoryFactory.getSdkNodeRepository(SDK_1, SDK_ROOT.resolve(SDK_1)); - assertTrue(ofSdk1.getSpecialPurpose().isEmpty()); - assertNull(ofSdk1.getSpecialPurposeNodeId("root")); + assertTrue(ofSdk1.getStableIds().isEmpty()); + assertNull(ofSdk1.getNodeIdByStableId("root")); } /** Each SDK version gets its own node implementation, as before. */ From d953d54594a888355c4456c1097c2407f24248db Mon Sep 17 00:00:00 2001 From: Ioannis Rousochatzakis Date: Mon, 12 Oct 2026 10:48:56 +0200 Subject: [PATCH 10/13] TEDEFO-5229: Use the SDK 2 names for the methods that read the disclosure control --- .../eu/europa/ted/eforms/sdk/entity/SdkField.java | 13 +++++++------ .../europa/ted/eforms/sdk/entity/v2/SdkFieldV2.java | 12 ++++++------ .../sdk/repository/SdkFieldRepositoryTest.java | 5 +++-- 3 files changed, 16 insertions(+), 14 deletions(-) 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 81dd387..74a6f33 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 @@ -138,8 +138,8 @@ protected SdkField(final JsonNode fieldNode) { this.type = fieldNode.get("type").asText(null); this.codelistId = extractCodelistId(fieldNode); this.repeatable = extractRepeatable(fieldNode); - this.privacyCode = this.extractPrivacyCode(fieldNode); - this.privacySettings = this.extractPrivacySettings(fieldNode); + this.privacyCode = this.extractDisclosureGroupId(fieldNode); + this.privacySettings = this.extractDisclosureControl(fieldNode); this.withholdingCondition = this.extractWithholdingCondition(fieldNode); this.undisclosedFieldSelector = this.extractUndisclosedFieldSelector(fieldNode); this.attributes = extractAttributes(fieldNode); @@ -188,15 +188,16 @@ protected boolean extractRepeatable(final JsonNode fieldNode) { } /** - * Returns the privacy code of the field, from the privacy block of fields/fields.json. SDK 2 - * gives it as the groupId of the disclosureControl block instead (TEDEFO-5229). + * 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 extractPrivacyCode(final JsonNode fieldNode) { + protected String extractDisclosureGroupId(final JsonNode fieldNode) { final JsonNode privacyNode = fieldNode.get("privacy"); return privacyNode != null ? privacyNode.get("code").asText(null) : null; } @@ -211,7 +212,7 @@ protected String extractPrivacyCode(final JsonNode fieldNode) { * and no state of its own. *

*/ - protected PrivacySettings extractPrivacySettings(final JsonNode fieldNode) { + protected PrivacySettings extractDisclosureControl(final JsonNode fieldNode) { return this.extractPrivacy(fieldNode.get("privacy")); } 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 ba5689d..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 @@ -73,13 +73,13 @@ protected boolean extractRepeatable(final JsonNode fieldNode) { } /** - * fields/fwd/fields.json gives the privacy code as the groupId of the disclosureControl block, - * where fields/fields.json gives it as the code of the privacy block. + * 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 extractPrivacyCode(final JsonNode fieldNode) { + protected String extractDisclosureGroupId(final JsonNode fieldNode) { if (disclosureControlOf(fieldNode) == null) { - return super.extractPrivacyCode(fieldNode); + return super.extractDisclosureGroupId(fieldNode); } return textOfDisclosureControl(fieldNode, "groupId"); } @@ -90,9 +90,9 @@ protected String extractPrivacyCode(final JsonNode fieldNode) { * read from the privacy block of fields/fields.json. */ @Override - protected PrivacySettings extractPrivacySettings(final JsonNode fieldNode) { + protected PrivacySettings extractDisclosureControl(final JsonNode fieldNode) { if (disclosureControlOf(fieldNode) == null) { - return super.extractPrivacySettings(fieldNode); + return super.extractDisclosureControl(fieldNode); } return null; } 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 index 2db2ab5..57e0852 100644 --- a/src/test/java/eu/europa/ted/eforms/sdk/repository/SdkFieldRepositoryTest.java +++ b/src/test/java/eu/europa/ted/eforms/sdk/repository/SdkFieldRepositoryTest.java @@ -93,9 +93,10 @@ void testRepeatableIsReadFromBothShapes() throws InstantiationException { assertFalse(ofSdk2.get("BT-NotRepeatable").isRepeatable()); } - /** SDK 2 gives the privacy code as the groupId of the disclosureControl block. */ + /** SDK 2 gives the disclosure group as the groupId of the disclosureControl block. */ @Test - void testThePrivacyCodeComesFromGroupIdForSdk2() throws InstantiationException { + 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(), From 7cad432c446236ba39181767f1a713f4781ca817 Mon Sep 17 00:00:00 2001 From: Ioannis Rousochatzakis Date: Mon, 12 Oct 2026 10:48:56 +0200 Subject: [PATCH 11/13] TEDEFO-5230: Use the SDK 2 name for the setter of the disclosure control --- src/main/java/eu/europa/ted/eforms/sdk/entity/SdkField.java | 4 ++-- .../ted/eforms/sdk/repository/v2/SdkFieldRepositoryV2.java | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) 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 74a6f33..0927529 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 @@ -353,8 +353,8 @@ public PrivacySettings getPrivacySettings() { * 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 setPrivacySettings(final PrivacySettings privacySettings) { - this.privacySettings = privacySettings; + public void setDisclosureControl(final PrivacySettings disclosureControl) { + this.privacySettings = disclosureControl; } /** 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 index 4f4eaac..4efadf8 100644 --- 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 @@ -71,7 +71,7 @@ private void linkTheDisclosureFields() { } for (final SdkField sdkField : this.values()) { if (sdkField.getPrivacyCode() != null) { - sdkField.setPrivacySettings(new SdkField.PrivacySettings( + sdkField.setDisclosureControl(new SdkField.PrivacySettings( this.getFieldIdByStableId(SdkConstants.STABLE_ID_DISCLOSURE_GROUP), this.getFieldIdByStableId(SdkConstants.STABLE_ID_DISCLOSURE_JUSTIFICATION_CODE), this.getFieldIdByStableId( From c56a0c10ec52e7250224ea83a8defd041b295663 Mon Sep 17 00:00:00 2001 From: Ioannis Rousochatzakis Date: Mon, 12 Oct 2026 11:01:50 +0200 Subject: [PATCH 12/13] TEDEFO-5229: Give the undisclosed field selector in its selector form --- src/main/java/eu/europa/ted/eforms/sdk/entity/SdkField.java | 4 ++-- .../ted/eforms/sdk/repository/SdkFieldRepositoryTest.java | 2 +- src/test/resources/sdk-fixture/2.0/fields/fwd/fields.json | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) 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 0927529..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 @@ -367,8 +367,8 @@ public String getWithholdingCondition() { } /** - * @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 + * @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() { 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 index 57e0852..40ffb6e 100644 --- a/src/test/java/eu/europa/ted/eforms/sdk/repository/SdkFieldRepositoryTest.java +++ b/src/test/java/eu/europa/ted/eforms/sdk/repository/SdkFieldRepositoryTest.java @@ -115,7 +115,7 @@ void testTheExpressionsOfTheConditionallyWithheldFields() throws InstantiationEx assertEquals("cro-bor-law", withheldSometimes.getPrivacyCode()); assertEquals("{BT-WithheldSometimes} ${BT-Plain == 'x'}", withheldSometimes.getWithholdingCondition()); - assertEquals("{ND-Root} ${BT-WithheldSometimes[BT-Plain == 'x']}", + assertEquals("{ND-Root} &{/BT-WithheldSometimes[BT-Plain == 'x']}", withheldSometimes.getUndisclosedFieldSelector()); } 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 index ba4b425..df6bf67 100644 --- a/src/test/resources/sdk-fixture/2.0/fields/fwd/fields.json +++ b/src/test/resources/sdk-fixture/2.0/fields/fwd/fields.json @@ -41,7 +41,7 @@ "disclosureControl" : { "groupId" : "cro-bor-law", "withholdingCondition" : "{BT-WithheldSometimes} ${BT-Plain == 'x'}", - "undisclosedFieldSelector" : "{ND-Root} ${BT-WithheldSometimes[BT-Plain == 'x']}" + "undisclosedFieldSelector" : "{ND-Root} &{/BT-WithheldSometimes[BT-Plain == 'x']}" } }, { "id" : "BT-Measure", From cf2158a2491911324b56940c1909f5a4477b1e84 Mon Sep 17 00:00:00 2001 From: Ioannis Rousochatzakis Date: Mon, 12 Oct 2026 11:01:50 +0200 Subject: [PATCH 13/13] TEDEFO-5228: Declare a node before its parent in the SDK 2 test nodes --- .../resources/sdk-fixture/2.0/fields/fwd/nodes.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) 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 index cf39e1c..adc565a 100644 --- a/src/test/resources/sdk-fixture/2.0/fields/fwd/nodes.json +++ b/src/test/resources/sdk-fixture/2.0/fields/fwd/nodes.json @@ -9,6 +9,12 @@ "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", @@ -16,11 +22,5 @@ "xpathRelative" : "cac:ProcurementProjectLot", "repeatable" : true, "alias" : "lot" - }, { - "id" : "ND-Fwd-Only", - "parentId" : "ND-Lot", - "xpathAbsolute" : "/*/cac:ProcurementProjectLot/cac:FwdOnly", - "xpathRelative" : "cac:FwdOnly", - "repeatable" : false } ] }