diff --git a/CHANGELOG.md b/CHANGELOG.md index d4632fe..a5eabc2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,12 +1,38 @@ -# eForms Core Library 1.5.0 Release Notes +# eForms Core Library 1.6.0 Release Notes -The eForms Core Library is a collection of utilities that are used by our sample applications as well as the EFX Toolkit for Java Developers. +The eForms Core Library is a collection of utilities used by the EFX Toolkit for Java Developers and other eForms applications. ## In this release -This release fixes an issue in the XPathProcessor that could cause a redundant predicate production when contextualising XPaths with multiple predicates. +### SDK entity improvements -The versions of various dependencies was updated: Apache Commons IO 2.19.0, Apache Commons Lang 3.18.0, Jackson 2.18.3, logback 1.5.18. +- Versioned SDK entity classes (`SdkFieldV1`, `SdkFieldV2`, `SdkNodeV1`, `SdkNodeV2`, etc.) have been moved from the EFX Toolkit into the core library, consolidating version-specific implementations in a single location. +- `SdkNode` now supports parent node references and ancestor chain traversal via `getAncestry()`. +- `SdkField` now exposes repeatability information, parent node references, and parsed XPath metadata via `getXpathInfo()`. +- Repository classes (`SdkNodeRepository`, `SdkFieldRepository`) now use two-pass loading to wire parent-child relationships during initialization. + +### Privacy and data type support + +- Added `PrivacySettings` to `SdkField`, providing access to privacy code, justification, publication date, and related field references. +- Introduced `SdkDataType` entity and `SdkDataTypeRepository` for field type-level metadata including privacy masking values. +- Separated `duration` as a distinct data type from `measure`. + +### Notice subtype management + +- Added `SdkNoticeSubtype` entity with intelligent ID parsing (prefix/number/suffix decomposition) and correct sorting order. +- Added `SdkNoticeTypeRepository` to load and manage notice subtypes. + +### Utilities + +- Moved `NoticeDocument` and `SafeDocumentBuilder` from the eforms-notice-viewer into the core library. `NoticeDocument` provides secure XML parsing with accessors for notice subtype, SDK version, and language detection. `SafeDocumentBuilder` implements XXE prevention following OWASP guidelines. + +### Component registry + +- Added component types for dependency extraction (`EFX_COMPUTE_DEPENDENCY_EXTRACTOR`, `EFX_VALIDATION_DEPENDENCY_EXTRACTOR`) and EFX rules translation (`EFX_RULES_TRANSLATOR`). + +### Dependencies + +- Updated versions of various dependencies. ## Download diff --git a/README.md b/README.md index 822ec49..0396b9f 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,8 @@ This library provides a set of classes that can be used to solve some common "pr * Automatically discovering and downloading new versions of the eForms SDK. * Maintaining and instantiating at runtime the correct application component versions for different major versions of the SDK. * Basic parsing and processing of XPath expressions. +* Parsing eForms notice XML documents and extracting metadata (SDK version, subtype, languages). +* Secure XML document building with XXE prevention (OWASP guidelines). ## Using the eForms Core Library diff --git a/pom.xml b/pom.xml index 2ec8943..90bb10f 100644 --- a/pom.xml +++ b/pom.xml @@ -3,7 +3,7 @@ eu.europa.ted.eforms eforms-core-java - 1.5.0 + 1.6.0 eForms Core Library API and tools for eForms applications. @@ -33,7 +33,7 @@ - 2024-08-02T09:50:45Z + 2025-07-30T08:40:55Z UTF-8 @@ -391,6 +391,9 @@ org.apache.maven.plugins maven-javadoc-plugin ${version.javadoc.plugin} + + all,-missing + org.apache.maven.plugins @@ -547,15 +550,17 @@ - - org.sonatype.central - central-publishing-maven-plugin - true - - central - true - - + + org.sonatype.central + central-publishing-maven-plugin + true + + central + true + + ${project.artifactId} ${project.version} + + diff --git a/src/main/java/eu/europa/ted/eforms/NoticeDocument.java b/src/main/java/eu/europa/ted/eforms/NoticeDocument.java new file mode 100644 index 0000000..c534856 --- /dev/null +++ b/src/main/java/eu/europa/ted/eforms/NoticeDocument.java @@ -0,0 +1,176 @@ +/* + * Copyright 2022 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; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.xpath.XPath; +import javax.xml.xpath.XPathExpressionException; +import javax.xml.xpath.XPathFactory; +import javax.xml.xpath.XPathNodes; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Validate; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; +import org.xml.sax.SAXException; +import eu.europa.ted.util.SafeDocumentBuilder; + +/** + * A class representing a Notice document with accessor methods for its XML contents and metadata. + */ +public class NoticeDocument { + + private static final String TAG_PRIMARY_LANGUAGE = "cbc:NoticeLanguageCode"; + private static final String TAG_SDK_VERSION = "cbc:CustomizationID"; + private static final String TAG_SUBTYPE_CODE = "cbc:SubTypeCode"; + private static final String XPATH_ADDITIONAL_LANGUAGE = + "/*/AdditionalNoticeLanguage/ID/text()"; + + private static final XPath xpath = XPathFactory.newInstance().newXPath(); + + private final Element root; + private final String xmlContents; + + public NoticeDocument(final Path noticeXmlPath) + throws ParserConfigurationException, SAXException, IOException { + Validate.notNull(noticeXmlPath, "Undefined Notice XML file path"); + + if (!Files.isRegularFile(noticeXmlPath)) { + throw new FileNotFoundException(noticeXmlPath.toString()); + } + + this.xmlContents = Files.readString(noticeXmlPath, StandardCharsets.UTF_8); + this.root = parseXmlRoot(this.xmlContents); + } + + public NoticeDocument(final InputStream noticeXmlInput) + throws ParserConfigurationException, SAXException, IOException { + Validate.notNull(noticeXmlInput, "Undefined Notice XML input"); + + this.xmlContents = new String(noticeXmlInput.readAllBytes(), StandardCharsets.UTF_8); + this.root = parseXmlRoot(this.xmlContents); + } + + public NoticeDocument(final String noticeXmlContents) + throws ParserConfigurationException, SAXException, IOException { + Validate.notBlank(noticeXmlContents, "Invalid Notice XML contents"); + + this.xmlContents = noticeXmlContents; + this.root = parseXmlRoot(this.xmlContents); + } + + private static Element parseXmlRoot(final String xmlContents) + throws ParserConfigurationException, SAXException, IOException { + try (InputStream input = + new java.io.ByteArrayInputStream(xmlContents.getBytes(StandardCharsets.UTF_8))) { + final Element root = + SafeDocumentBuilder.buildSafeDocumentBuilderAllowDoctype().parse(input) + .getDocumentElement(); + Validate.notNull(root, "No XML root found"); + return root; + } + } + + /** + * Gets the notice sub type from the notice XML. + * + * @return The notice sub type as found in the notice XML + */ + public String getNoticeSubType() { + return Optional.ofNullable(this.root.getElementsByTagName(TAG_SUBTYPE_CODE)) + .map((final NodeList subTypeCodes) -> { + Optional result = Optional.empty(); + for (int i = 0; i < subTypeCodes.getLength(); i++) { + result = Optional.ofNullable(subTypeCodes.item(i)) + .filter((final Node node) -> node.getAttributes() != null) + .map(Node::getTextContent) + .map(StringUtils::strip); + } + return result.orElse(null); + }) + .filter(StringUtils::isNotBlank) + .orElseThrow(() -> new RuntimeException("SubTypeCode not found in notice XML")); + } + + /** + * Gets the eForms SDK version from the notice XML. + * + * @return The eForms SDK version as found in the notice XML + */ + public String getEformsSdkVersion() { + return Optional.ofNullable(this.root.getElementsByTagName(TAG_SDK_VERSION)) + .filter((final NodeList nodes) -> nodes.getLength() == 1) + .map((final NodeList nodes) -> Optional.ofNullable(nodes.item(0)) + .map(Node::getTextContent) + .map(StringUtils::strip) + .map((final String str) -> str.startsWith("eforms-sdk-") + ? str.substring("eforms-sdk-".length()) : str) + .orElse(null)) + .filter(StringUtils::isNotBlank) + .orElseThrow(() -> new RuntimeException("eForms SDK version not found in notice XML")); + } + + /** + * Gets the primary language from the notice XML. + * + * @return The primary language + */ + public String getPrimaryLanguage() { + return Optional + .ofNullable(this.root.getElementsByTagName(TAG_PRIMARY_LANGUAGE)) + .map((final NodeList nodes) -> nodes.item(0)) + .map(Node::getTextContent) + .orElse(null); + } + + /** + * Gets the list of other languages from the notice XML. + * + * @return A list of other languages + * @throws XPathExpressionException If an error occurs evaluating the XPath expression + */ + public List getOtherLanguages() throws XPathExpressionException { + return Optional + .ofNullable(xpath.evaluateExpression(XPATH_ADDITIONAL_LANGUAGE, + this.root.getOwnerDocument(), XPathNodes.class)) + .map((final XPathNodes nodes) -> { + final List languages = new ArrayList<>(); + nodes.forEach((final Node node) -> { + if (StringUtils.isNotBlank(node.getTextContent())) { + languages.add(node.getTextContent()); + } + }); + return languages; + }) + .orElseGet(ArrayList::new); + } + + /** + * Gets the notice XML contents. + * + * @return The notice XML + */ + public String getXmlContents() { + return this.xmlContents; + } +} diff --git a/src/main/java/eu/europa/ted/eforms/sdk/README.md b/src/main/java/eu/europa/ted/eforms/sdk/README.md index 39fdd00..32102b1 100644 --- a/src/main/java/eu/europa/ted/eforms/sdk/README.md +++ b/src/main/java/eu/europa/ted/eforms/sdk/README.md @@ -4,6 +4,8 @@ The `eu.europa.ted.eforms.sdk` package contains the core classes and packages th The main packages included here are: -* `component`: Provides a solution for handling multiple major versions of the SDK in parallel. +* `component`: Provides a solution for handling multiple major versions of the SDK in parallel. +* `entity`: Provides abstract entity classes for representing SDK metadata (fields, nodes, codelists, notice subtypes, data types). +* `repository`: Provides classes for reading SDK entities from JSON and Genericode files. * `resource`: Provides a solution for automatically discovering and downloading new versions of the eForms SDK. 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 deeeb97..25ecad1 100644 --- a/src/main/java/eu/europa/ted/eforms/sdk/SdkConstants.java +++ b/src/main/java/eu/europa/ted/eforms/sdk/SdkConstants.java @@ -7,6 +7,7 @@ public class SdkConstants { public static final String FIELDS_JSON_XML_STRUCTURE_KEY = "xmlStructure"; public static final String FIELDS_JSON_FIELDS_KEY = "fields"; + public static final String NOTICE_TYPES_JSON_SUBTYPES_KEY = "noticeSubTypes"; public static final String NOTICE_TYPES_JSON_DOCUMENT_TYPES_KEY = "documentTypes"; public static final String NOTICE_TYPES_JSON_DOCUMENT_TYPE_KEY = "documentType"; public static final String NOTICE_TYPES_JSON_NAMESPACE_KEY = "namespace"; diff --git a/src/main/java/eu/europa/ted/eforms/sdk/SdkVersion.java b/src/main/java/eu/europa/ted/eforms/sdk/SdkVersion.java index d539f00..d7971f4 100644 --- a/src/main/java/eu/europa/ted/eforms/sdk/SdkVersion.java +++ b/src/main/java/eu/europa/ted/eforms/sdk/SdkVersion.java @@ -14,11 +14,15 @@ public class SdkVersion implements Comparable { private final Semver version; + private static final String SDK_PREFIX = "eforms-sdk-"; + public SdkVersion(final String version) { Validate.notBlank(version, "Undefined version"); + String normalized = version.startsWith(SDK_PREFIX) ? version.substring(SDK_PREFIX.length()) : version; + // LOOSE because we need to accept MAJOR.MINOR - this.version = new Semver(version, SemverType.LOOSE); + this.version = new Semver(normalized, SemverType.LOOSE); // Check that we did get a MINOR part Validate.notNull(this.version.getMinor()); diff --git a/src/main/java/eu/europa/ted/eforms/sdk/component/SdkComponent.java b/src/main/java/eu/europa/ted/eforms/sdk/component/SdkComponent.java index 8ac3f56..ebc73de 100644 --- a/src/main/java/eu/europa/ted/eforms/sdk/component/SdkComponent.java +++ b/src/main/java/eu/europa/ted/eforms/sdk/component/SdkComponent.java @@ -6,6 +6,11 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +/** + * Marks a class as an SDK component implementation for specific SDK versions. + * Each annotated class must correspond to a specific component type and can optionally + * specify a qualifier for multiple implementations of the same type. + */ @Inherited @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) diff --git a/src/main/java/eu/europa/ted/eforms/sdk/component/SdkComponentDescriptor.java b/src/main/java/eu/europa/ted/eforms/sdk/component/SdkComponentDescriptor.java index ffb44a4..a2629c3 100644 --- a/src/main/java/eu/europa/ted/eforms/sdk/component/SdkComponentDescriptor.java +++ b/src/main/java/eu/europa/ted/eforms/sdk/component/SdkComponentDescriptor.java @@ -14,6 +14,10 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +/** + * Descriptor that uniquely identifies an SDK component by its version, type, and qualifier. + * Used internally by {@link SdkComponentFactory} for component registry and lookup. + */ public class SdkComponentDescriptor implements Serializable { private static final long serialVersionUID = -6237218459963821365L; @@ -27,11 +31,13 @@ public class SdkComponentDescriptor implements Serializable { private Class implType; - public SdkComponentDescriptor(String sdkVersion, SdkComponentType componentType, - Class implType) { - this(sdkVersion, componentType, "", implType); - } - + /** + * Creates a descriptor with the specified SDK version, component type, and qualifier. + * + * @param sdkVersion the SDK version + * @param componentType the component type + * @param qualifier the qualifier (use empty string for default components) + */ public SdkComponentDescriptor(String sdkVersion, SdkComponentType componentType, String qualifier, Class implType) { this.sdkVersion = Validate.notBlank(sdkVersion, "Undefined SDK version"); @@ -40,6 +46,18 @@ public SdkComponentDescriptor(String sdkVersion, SdkComponentType componentType, this.implType = Validate.notNull(implType, "Undefined implementation type"); } + /** + * Creates a descriptor with the specified SDK version and component type. + * The qualifier defaults to empty string. + * + * @param sdkVersion the SDK version + * @param componentType the component type + */ + public SdkComponentDescriptor(String sdkVersion, SdkComponentType componentType, + Class implType) { + this(sdkVersion, componentType, "", implType); + } + @SuppressWarnings("unchecked") public T createInstance(Object... initArgs) throws InstantiationException { try { diff --git a/src/main/java/eu/europa/ted/eforms/sdk/component/SdkComponentFactory.java b/src/main/java/eu/europa/ted/eforms/sdk/component/SdkComponentFactory.java index 5c91764..80a3482 100644 --- a/src/main/java/eu/europa/ted/eforms/sdk/component/SdkComponentFactory.java +++ b/src/main/java/eu/europa/ted/eforms/sdk/component/SdkComponentFactory.java @@ -10,6 +10,7 @@ import java.util.Objects; import java.util.Optional; import java.util.stream.Stream; +import eu.europa.ted.eforms.sdk.SdkVersion; import org.reflections.Reflections; import org.reflections.util.ClasspathHelper; import org.reflections.util.ConfigurationBuilder; @@ -174,19 +175,11 @@ protected T getComponentImpl(String sdkVersion, final SdkComponentType compo } private static String normalizeVersion(final String sdkVersion) { - String normalizedVersion = sdkVersion; - - if (normalizedVersion.startsWith("eforms-sdk-")) { - normalizedVersion = normalizedVersion.substring(11); + SdkVersion version = new SdkVersion(sdkVersion); + int major = Integer.parseInt(version.getMajor()); + if (major > 0) { + return version.getMajor(); } - - String[] numbers = normalizedVersion.split("\\.", -2); - - if (numbers.length < 1) { - throw new IllegalArgumentException("Invalid SDK version: " + sdkVersion); - } - - return numbers[0] - + ((numbers.length > 1 && Integer.parseInt(numbers[0]) > 0) ? "" : "." + numbers[1]); + return version.getMajor() + "." + version.getMinor(); } } 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 a63254d..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 @@ -1,5 +1,11 @@ package eu.europa.ted.eforms.sdk.component; +/** + * Enumeration of component types that can be registered with the SDK component + * factory. + */ public enum SdkComponentType { - FIELD, NODE, CODELIST, EFX_EXPRESSION_TRANSLATOR, EFX_TEMPLATE_TRANSLATOR, SYMBOL_RESOLVER, SCRIPT_GENERATOR, MARKUP_GENERATOR; + 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/component/package-info.java b/src/main/java/eu/europa/ted/eforms/sdk/component/package-info.java new file mode 100644 index 0000000..1def354 --- /dev/null +++ b/src/main/java/eu/europa/ted/eforms/sdk/component/package-info.java @@ -0,0 +1,8 @@ +/** + * Provides an abstract factory pattern implementation for managing SDK version-specific components. + * This package allows multiple implementations of the same component type to coexist, with the + * appropriate implementation selected based on SDK version and optional qualifier. + * + * @since 1.0.0 + */ +package eu.europa.ted.eforms.sdk.component; \ No newline at end of file diff --git a/src/main/java/eu/europa/ted/eforms/sdk/entity/README.md b/src/main/java/eu/europa/ted/eforms/sdk/entity/README.md index 1ca638c..ff413cf 100644 --- a/src/main/java/eu/europa/ted/eforms/sdk/entity/README.md +++ b/src/main/java/eu/europa/ted/eforms/sdk/entity/README.md @@ -1,13 +1,14 @@ # Common Entities -The entities in this package can be used while reading data from the eForms SDK. Currently there are only three entities implemented here: +The entities in this package can be used while reading data from the eForms SDK. The following entities are implemented here: -* `SdkField`: Can hold basic information about a field. -* `SdkNode`: Can hold basic information about a node and reconstruct the node hierarchy. +* `SdkField`: Can hold basic information about a field, including repeatability, parent node, XPath metadata, and privacy settings. +* `SdkNode`: Can hold basic information about a node and reconstruct the node hierarchy via parent references and ancestor chain traversal. * `SdkCodelist`: Can hold codelist information including its codes. +* `SdkNoticeSubtype`: Can hold information about a notice subtype from the SDK's notice-types.json file. +* `SdkDataType`: Can hold field type-level metadata including privacy masking values. All the classes are abstract so that they can have specific implementations for different major versions of the eForms SDK if needed. This package also includes a factory class (`SdkEntityFactory`) that is meant to be used for instantiating concrete implementations of these abstract entity classes for different major versions of the eForms SDK. -_There is no rocket science in the code in this package. You are welcome to reuse it. It is intended to be used primarily by the EFX Toolkit and our sample applications._ 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 new file mode 100644 index 0000000..396eb57 --- /dev/null +++ b/src/main/java/eu/europa/ted/eforms/sdk/entity/SdkDataType.java @@ -0,0 +1,68 @@ +/* + * 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; + +import java.util.Objects; + +/** + * Represents an eForms SDK data type. + * + * 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. Currently hardcoded; will be loaded from + * data-types.json when it is added to the SDK. + */ +public class SdkDataType { + private final String id; + private final String privacyMask; + + @SuppressWarnings("unused") + private SdkDataType() { + throw new UnsupportedOperationException(); + } + + public SdkDataType(final String id, final String privacyMask) { + this.id = id; + this.privacyMask = privacyMask; + } + + public String getId() { + return this.id; + } + + public String getPrivacyMask() { + return this.privacyMask; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + SdkDataType other = (SdkDataType) obj; + return Objects.equals(this.id, other.id); + } + + @Override + public int hashCode() { + return Objects.hash(this.id); + } + + @Override + public String toString() { + return this.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 36f9f8b..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 @@ -30,4 +30,10 @@ public static SdkNode getSdkNode(String sdkVersion, JsonNode node) throws Instan return SdkEntityFactory.INSTANCE.getComponentImpl(sdkVersion, SdkComponentType.NODE, SdkNode.class, node); } + + public static SdkNoticeSubtype getSdkNoticeSubtype(final String sdkVersion, final JsonNode json) + throws InstantiationException { + return SdkEntityFactory.INSTANCE.getComponentImpl(sdkVersion, SdkComponentType.NOTICE_TYPE, + SdkNoticeSubtype.class, 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 49e8be6..45b67ae 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 @@ -1,7 +1,12 @@ package eu.europa.ted.eforms.sdk.entity; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; import java.util.Objects; import com.fasterxml.jackson.databind.JsonNode; +import eu.europa.ted.eforms.xpath.XPathInfo; +import eu.europa.ted.eforms.xpath.XPathProcessor; public abstract class SdkField implements Comparable { private final String id; @@ -10,6 +15,87 @@ public abstract class SdkField implements Comparable { private final String parentNodeId; private final String type; private final String codelistId; + private final boolean repeatable; + private final String privacyCode; + private final PrivacySettings privacySettings; + private final List attributes; + private final String attributeOf; + private final String attributeName; + private SdkNode parentNode; + private List attributeFields; + private SdkField attributeOfField; + private XPathInfo xpathInfo; + + /** + * Privacy settings for fields that can be withheld from publication. + */ + public static class PrivacySettings { + private final String privacyCodeFieldId; + private final String justificationCodeFieldId; + private final String justificationDescriptionFieldId; + private final String publicationDateFieldId; + private SdkField privacyCodeField; + private SdkField justificationCodeField; + private SdkField justificationDescriptionField; + private SdkField publicationDateField; + + public PrivacySettings(final String privacyCodeFieldId, + final String justificationCodeFieldId, final String justificationDescriptionFieldId, + final String publicationDateFieldId) { + this.privacyCodeFieldId = privacyCodeFieldId; + this.justificationCodeFieldId = justificationCodeFieldId; + this.justificationDescriptionFieldId = justificationDescriptionFieldId; + this.publicationDateFieldId = publicationDateFieldId; + } + + public String getPrivacyCodeFieldId() { + return this.privacyCodeFieldId; + } + + public String getJustificationCodeFieldId() { + return this.justificationCodeFieldId; + } + + public String getJustificationDescriptionFieldId() { + return this.justificationDescriptionFieldId; + } + + public String getPublicationDateFieldId() { + return this.publicationDateFieldId; + } + + public SdkField getPrivacyCodeField() { + return this.privacyCodeField; + } + + public void setPrivacyCodeField(SdkField privacyCodeField) { + this.privacyCodeField = privacyCodeField; + } + + public SdkField getJustificationCodeField() { + return this.justificationCodeField; + } + + public void setJustificationCodeField(SdkField justificationCodeField) { + this.justificationCodeField = justificationCodeField; + } + + public SdkField getJustificationDescriptionField() { + return this.justificationDescriptionField; + } + + public void setJustificationDescriptionField(SdkField justificationDescriptionField) { + this.justificationDescriptionField = justificationDescriptionField; + } + + public SdkField getPublicationDateField() { + return this.publicationDateField; + } + + public void setPublicationDateField(SdkField publicationDateField) { + this.publicationDateField = publicationDateField; + } + } @SuppressWarnings("unused") private SdkField() { @@ -18,12 +104,24 @@ private SdkField() { protected SdkField(final String id, final String type, final String parentNodeId, final String xpathAbsolute, final String xpathRelative, final String codelistId) { + this(id, type, parentNodeId, xpathAbsolute, xpathRelative, codelistId, false); + } + + protected SdkField(final String id, final String type, final String parentNodeId, + final String xpathAbsolute, final String xpathRelative, final String codelistId, + final boolean repeatable) { this.id = id; this.parentNodeId = parentNodeId; this.xpathAbsolute = xpathAbsolute; this.xpathRelative = xpathRelative; this.type = type; this.codelistId = codelistId; + this.repeatable = repeatable; + this.privacyCode = null; + this.privacySettings = null; + this.attributes = Collections.emptyList(); + this.attributeOf = null; + this.attributeName = null; } protected SdkField(final JsonNode fieldNode) { @@ -33,6 +131,25 @@ protected SdkField(final JsonNode fieldNode) { this.xpathRelative = fieldNode.get("xpathRelative").asText(null); 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.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; + } + + protected List extractAttributes(final JsonNode fieldNode) { + final JsonNode attributesNode = fieldNode.get("attributes"); + if (attributesNode == null || !attributesNode.isArray()) { + return Collections.emptyList(); + } + List result = new ArrayList<>(); + for (JsonNode attr : attributesNode) { + result.add(attr.asText()); + } + return Collections.unmodifiableList(result); } protected String extractCodelistId(final JsonNode fieldNode) { @@ -49,28 +166,133 @@ protected String extractCodelistId(final JsonNode fieldNode) { return valueNode.get("id").asText(null); } + protected boolean extractRepeatable(final JsonNode fieldNode) { + final JsonNode repeatableNode = fieldNode.get("repeatable"); + if (repeatableNode == null) { + return false; + } + + final JsonNode valueNode = repeatableNode.get("value"); + if (valueNode == null) { + return false; + } + + return valueNode.asBoolean(false); + } + + protected PrivacySettings extractPrivacy(final JsonNode privacyNode) { + if (privacyNode == null) { + return null; + } + + final String privacyCodeFieldId = privacyNode.get("unpublishedFieldId").asText(null); + final String justificationCodeFieldId = privacyNode.get("reasonCodeFieldId").asText(null); + final String justificationDescriptionFieldId = + privacyNode.get("reasonDescriptionFieldId").asText(null); + final String publicationDateFieldId = privacyNode.get("publicationDateFieldId").asText(null); + + return new PrivacySettings(privacyCodeFieldId, justificationCodeFieldId, + justificationDescriptionFieldId, publicationDateFieldId); + } + public String getId() { - return id; + return this.id; } public String getParentNodeId() { - return parentNodeId; + return this.parentNodeId; } public String getXpathAbsolute() { - return xpathAbsolute; + return this.xpathAbsolute; } public String getXpathRelative() { - return xpathRelative; + return this.xpathRelative; } public String getType() { - return type; + return this.type; } public String getCodelistId() { - return codelistId; + return this.codelistId; + } + + public List getAttributes() { + return this.attributes; + } + + public String getAttributeOf() { + return this.attributeOf; + } + + public String getAttributeName() { + return this.attributeName; + } + + public List getAttributeFields() { + return this.attributeFields; + } + + public void setAttributeFields(List attributeFields) { + this.attributeFields = Collections.unmodifiableList(attributeFields); + } + + public SdkField getAttributeOfField() { + return this.attributeOfField; + } + + public void setAttributeOfField(SdkField attributeOfField) { + this.attributeOfField = attributeOfField; + } + + /** + * Returns the attribute field with the given XML attribute name (e.g. "unitCode", "listName"), + * or null if this field has no such attribute. + */ + public SdkField getAttributeField(String attrName) { + if (this.attributeFields == null) { + return null; + } + for (SdkField attrField : this.attributeFields) { + if (attrName.equals(attrField.getAttributeName())) { + return attrField; + } + } + return null; + } + + public boolean isRepeatable() { + return this.repeatable; + } + + public String getPrivacyCode() { + return this.privacyCode; + } + + public PrivacySettings getPrivacySettings() { + return this.privacySettings; + } + + public SdkNode getParentNode() { + return this.parentNode; + } + + public void setParentNode(SdkNode parentNode) { + this.parentNode = parentNode; + } + + /** + * Returns parsed XPath information for this field. + * Provides access to attribute info, path decomposition, and predicate checks. + * Lazily initialized on first access. + */ + public XPathInfo getXpathInfo() { + if (this.xpathInfo == null) { + this.xpathInfo = XPathProcessor.parse(this.xpathAbsolute); + } + return this.xpathInfo; } /** @@ -93,16 +315,16 @@ public boolean equals(Object obj) { return false; } SdkField other = (SdkField) obj; - return Objects.equals(id, other.id); + return Objects.equals(this.id, other.id); } @Override public int hashCode() { - return Objects.hash(id); + return Objects.hash(this.id); } @Override public String toString() { - return "SdkField [id=" + id + "]"; + return this.id; } } diff --git a/src/main/java/eu/europa/ted/eforms/sdk/entity/SdkNode.java b/src/main/java/eu/europa/ted/eforms/sdk/entity/SdkNode.java index 9166eaf..1057b4a 100644 --- a/src/main/java/eu/europa/ted/eforms/sdk/entity/SdkNode.java +++ b/src/main/java/eu/europa/ted/eforms/sdk/entity/SdkNode.java @@ -1,5 +1,8 @@ package eu.europa.ted.eforms.sdk.entity; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; import java.util.Objects; import com.fasterxml.jackson.databind.JsonNode; @@ -12,6 +15,8 @@ public abstract class SdkNode implements Comparable { private final String xpathRelative; private final String parentId; private final boolean repeatable; + private SdkNode parent; + private List cachedAncestry; protected SdkNode(final String id, final String parentId, final String xpathAbsolute, final String xpathRelative, final boolean repeatable) { @@ -51,9 +56,46 @@ public boolean isRepeatable() { return repeatable; } + public SdkNode getParent() { + return parent; + } + + /** + * Sets the parent node and invalidates the cached ancestry. + * Should only be called during SDK initialization (two-pass loading). + * + * @param parent the parent node + */ + public void setParent(SdkNode parent) { + this.parent = parent; + this.cachedAncestry = null; + } + + /** + * Returns the ancestry chain from this node to the root. + * The list includes this node as the first element, followed by its parent, + * grandparent, and so on up to the root node. + * + * The result is cached and recomputed only when the parent changes. + * + * @return unmodifiable list of node IDs ordered from child (this node) to root + */ + public List getAncestry() { + if (cachedAncestry == null) { + List ancestry = new ArrayList<>(); + SdkNode current = this; + while (current != null) { + ancestry.add(current.getId()); + current = current.getParent(); + } + cachedAncestry = Collections.unmodifiableList(ancestry); + } + return cachedAncestry; + } + @Override public int compareTo(SdkNode o) { - return o.getId().compareTo(o.getId()); + return this.getId().compareTo(o.getId()); } @Override diff --git a/src/main/java/eu/europa/ted/eforms/sdk/entity/SdkNoticeSubtype.java b/src/main/java/eu/europa/ted/eforms/sdk/entity/SdkNoticeSubtype.java new file mode 100644 index 0000000..d7a1708 --- /dev/null +++ b/src/main/java/eu/europa/ted/eforms/sdk/entity/SdkNoticeSubtype.java @@ -0,0 +1,102 @@ +package eu.europa.ted.eforms.sdk.entity; + +import java.util.Objects; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import com.fasterxml.jackson.databind.JsonNode; + +/** + * Represents a notice subtype from the SDK's notice-types.json file. + */ +public abstract class SdkNoticeSubtype implements Comparable { + private static final Pattern ID_PATTERN = Pattern.compile("^([A-Za-z_-]*)(\\d+)([A-Za-z_-][A-Za-z0-9_-]*)?$"); + + private final String subTypeId; + private final String documentType; + private final String type; + private final String prefix; + private final int number; + private final String suffix; + + protected SdkNoticeSubtype(String subTypeId, String documentType, String type) { + this.subTypeId = subTypeId; + this.documentType = documentType; + this.type = type; + + Matcher m = ID_PATTERN.matcher(subTypeId != null ? subTypeId : ""); + if (m.matches()) { + this.prefix = m.group(1); + this.number = Integer.parseInt(m.group(2)); + this.suffix = m.group(3) != null ? m.group(3) : ""; + } else { + this.prefix = subTypeId != null ? subTypeId : ""; + this.number = 0; + this.suffix = ""; + } + } + + protected SdkNoticeSubtype(JsonNode json) { + this(json.get("subTypeId").asText(null), + json.get("documentType").asText(null), + json.get("type").asText(null)); + } + + /** + * Returns the notice subtype ID (e.g., "1", "3", "CEI", "E1", "X01"). + * This is the primary identifier used for phase generation. + */ + public String getId() { + return subTypeId; + } + + public String getSubTypeId() { + return subTypeId; + } + + public String getDocumentType() { + return documentType; + } + + public String getType() { + return type; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + SdkNoticeSubtype other = (SdkNoticeSubtype) obj; + return Objects.equals(subTypeId, other.subTypeId); + } + + @Override + public int compareTo(SdkNoticeSubtype o) { + int cmp = this.prefix.compareTo(o.prefix); + if (cmp != 0) { + return cmp; + } + cmp = Integer.compare(this.number, o.number); + if (cmp != 0) { + return cmp; + } + return this.suffix.compareTo(o.suffix); + } + + @Override + public int hashCode() { + return Objects.hash(subTypeId); + } + + @Override + public String toString() { + return subTypeId; + } +} diff --git a/src/main/java/eu/europa/ted/eforms/sdk/entity/v1/SdkCodelistV1.java b/src/main/java/eu/europa/ted/eforms/sdk/entity/v1/SdkCodelistV1.java new file mode 100644 index 0000000..d186edc --- /dev/null +++ b/src/main/java/eu/europa/ted/eforms/sdk/entity/v1/SdkCodelistV1.java @@ -0,0 +1,21 @@ +package eu.europa.ted.eforms.sdk.entity.v1; + +import java.util.List; +import java.util.Optional; +import eu.europa.ted.eforms.sdk.component.SdkComponent; +import eu.europa.ted.eforms.sdk.component.SdkComponentType; +import eu.europa.ted.eforms.sdk.entity.SdkCodelist; + +/** + * Representation of an SdkCodelist for usage in the symbols map. + * + * @author rouschr + */ +@SdkComponent(versions = {"1"}, componentType = SdkComponentType.CODELIST) +public class SdkCodelistV1 extends SdkCodelist { + + public SdkCodelistV1(final String codelistId, final String codelistVersion, + final List codes, final Optional parentId) { + super(codelistId, codelistVersion, codes, parentId); + } +} diff --git a/src/main/java/eu/europa/ted/eforms/sdk/entity/v1/SdkFieldV1.java b/src/main/java/eu/europa/ted/eforms/sdk/entity/v1/SdkFieldV1.java new file mode 100644 index 0000000..0a7457b --- /dev/null +++ b/src/main/java/eu/europa/ted/eforms/sdk/entity/v1/SdkFieldV1.java @@ -0,0 +1,65 @@ +package eu.europa.ted.eforms.sdk.entity.v1; + +import java.util.Map; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +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.SdkField; + +@SdkComponent(versions = {"1"}, componentType = SdkComponentType.FIELD) +public class SdkFieldV1 extends SdkField { + + public SdkFieldV1(final String id, final String type, final String parentNodeId, + final String xpathAbsolute, final String xpathRelative, final String codelistId, + final boolean repeatable) { + super(id, type, parentNodeId, xpathAbsolute, xpathRelative, codelistId, repeatable); + } + + public SdkFieldV1(final JsonNode field) { + super(field); + } + + @JsonCreator + public SdkFieldV1( + @JsonProperty("id") final String id, + @JsonProperty("type") final String type, + @JsonProperty("parentNodeId") final String parentNodeId, + @JsonProperty("xpathAbsolute") final String xpathAbsolute, + @JsonProperty("xpathRelative") final String xpathRelative, + @JsonProperty("codeList") final Map> codelist, + @JsonProperty("repeatable") final Map repeatable) { + this(id, type, parentNodeId, xpathAbsolute, xpathRelative, getCodelistId(codelist), + getRepeatable(repeatable)); + } + + protected static String getCodelistId(Map> codelist) { + if (codelist == null) { + return null; + } + + Map value = codelist.get("value"); + if (value == null) { + return null; + } + + return value.get("id"); + } + + protected static boolean getRepeatable(Map repeatable) { + if (repeatable == null) { + return false; + } + + // If there are constraints, the field may repeat conditionally - treat as repeatable + Object constraints = repeatable.get("constraints"); + if (constraints != null) { + return true; + } + + // Otherwise check the default value + Object value = repeatable.get("value"); + return Boolean.TRUE.equals(value); + } +} diff --git a/src/main/java/eu/europa/ted/eforms/sdk/entity/v1/SdkNodeV1.java b/src/main/java/eu/europa/ted/eforms/sdk/entity/v1/SdkNodeV1.java new file mode 100644 index 0000000..69de45c --- /dev/null +++ b/src/main/java/eu/europa/ted/eforms/sdk/entity/v1/SdkNodeV1.java @@ -0,0 +1,29 @@ +package eu.europa.ted.eforms.sdk.entity.v1; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +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.SdkNode; + +/** + * A node is something like a section. Nodes can be parents of other nodes or parents of fields. + */ +@SdkComponent(versions = {"1"}, componentType = SdkComponentType.NODE) +public class SdkNodeV1 extends SdkNode { + + @JsonCreator + public SdkNodeV1( + @JsonProperty("id") String id, + @JsonProperty("parentId") String parentId, + @JsonProperty("xpathAbsolute") String xpathAbsolute, + @JsonProperty("xpathRelative") String xpathRelative, + @JsonProperty("repeatable") boolean repeatable) { + super(id, parentId, xpathAbsolute, xpathRelative, repeatable); + } + + public SdkNodeV1(JsonNode node) { + super(node); + } +} diff --git a/src/main/java/eu/europa/ted/eforms/sdk/entity/v1/SdkNoticeSubtypeV1.java b/src/main/java/eu/europa/ted/eforms/sdk/entity/v1/SdkNoticeSubtypeV1.java new file mode 100644 index 0000000..2175b03 --- /dev/null +++ b/src/main/java/eu/europa/ted/eforms/sdk/entity/v1/SdkNoticeSubtypeV1.java @@ -0,0 +1,21 @@ +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.SdkNoticeSubtype; + +/** + * Represents a notice subtype from the SDK's notice-types.json file. + */ +@SdkComponent(versions = {"1"}, componentType = SdkComponentType.NOTICE_TYPE) +public class SdkNoticeSubtypeV1 extends SdkNoticeSubtype { + + public SdkNoticeSubtypeV1(String subTypeId, String documentType, String type) { + super(subTypeId, documentType, type); + } + + public SdkNoticeSubtypeV1(JsonNode json) { + super(json); + } +} diff --git a/src/main/java/eu/europa/ted/eforms/sdk/entity/v2/SdkCodelistV2.java b/src/main/java/eu/europa/ted/eforms/sdk/entity/v2/SdkCodelistV2.java new file mode 100644 index 0000000..bc4249d --- /dev/null +++ b/src/main/java/eu/europa/ted/eforms/sdk/entity/v2/SdkCodelistV2.java @@ -0,0 +1,21 @@ +package eu.europa.ted.eforms.sdk.entity.v2; + +import java.util.List; +import java.util.Optional; +import eu.europa.ted.eforms.sdk.component.SdkComponent; +import eu.europa.ted.eforms.sdk.component.SdkComponentType; +import eu.europa.ted.eforms.sdk.entity.v1.SdkCodelistV1; + +/** + * Representation of an SdkCodelist for usage in the symbols map. + * + * @author rouschr + */ +@SdkComponent(versions = {"2"}, componentType = SdkComponentType.CODELIST) +public class SdkCodelistV2 extends SdkCodelistV1 { + + public SdkCodelistV2(final String codelistId, final String codelistVersion, + final List codes, final Optional parentId) { + super(codelistId, codelistVersion, codes, parentId); + } +} 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 new file mode 100644 index 0000000..d69ad30 --- /dev/null +++ b/src/main/java/eu/europa/ted/eforms/sdk/entity/v2/SdkFieldV2.java @@ -0,0 +1,43 @@ +package eu.europa.ted.eforms.sdk.entity.v2; + +import java.util.Map; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +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.SdkFieldV1; + +@SdkComponent(versions = {"2"}, componentType = SdkComponentType.FIELD) +public class SdkFieldV2 extends SdkFieldV1 { + private final String alias; + + public SdkFieldV2(String id, String type, String parentNodeId, String xpathAbsolute, + String xpathRelative, String rootCodelistId, boolean repeatable, String alias) { + super(id, type, parentNodeId, xpathAbsolute, xpathRelative, rootCodelistId, repeatable); + this.alias = alias; + } + + public SdkFieldV2(JsonNode fieldNode) { + super(fieldNode); + this.alias = fieldNode.has("alias") ? fieldNode.get("alias").asText(null) : null; + } + + @JsonCreator + public SdkFieldV2( + @JsonProperty("id") final String id, + @JsonProperty("type") final String type, + @JsonProperty("parentNodeId") final String parentNodeId, + @JsonProperty("xpathAbsolute") final String xpathAbsolute, + @JsonProperty("xpathRelative") final String xpathRelative, + @JsonProperty("codeList") final Map> codelist, + @JsonProperty("repeatable") final Map repeatable, + @JsonProperty("alias") final String alias) { + this(id, type, parentNodeId, xpathAbsolute, xpathRelative, getCodelistId(codelist), + getRepeatable(repeatable), alias); + } + + public String getAlias() { + return alias; + } +} diff --git a/src/main/java/eu/europa/ted/eforms/sdk/entity/v2/SdkNodeV2.java b/src/main/java/eu/europa/ted/eforms/sdk/entity/v2/SdkNodeV2.java new file mode 100644 index 0000000..9d98cee --- /dev/null +++ b/src/main/java/eu/europa/ted/eforms/sdk/entity/v2/SdkNodeV2.java @@ -0,0 +1,37 @@ +package eu.europa.ted.eforms.sdk.entity.v2; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +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.SdkNodeV1; + +/** + * A node is something like a section. Nodes can be parents of other nodes or parents of fields. + */ +@SdkComponent(versions = {"2"}, componentType = SdkComponentType.NODE) +public class SdkNodeV2 extends SdkNodeV1 { + private final String alias; + + @JsonCreator + public SdkNodeV2( + @JsonProperty("id") String id, + @JsonProperty("parentId") String parentId, + @JsonProperty("xpathAbsolute") String xpathAbsolute, + @JsonProperty("xpathRelative") String xpathRelative, + @JsonProperty("repeatable") boolean repeatable, + @JsonProperty("alias") String alias) { + super(id, parentId, xpathAbsolute, xpathRelative, repeatable); + this.alias = alias; + } + + public SdkNodeV2(JsonNode node) { + super(node); + this.alias = node.has("alias") ? node.get("alias").asText(null) : null; + } + + public String getAlias() { + return alias; + } +} diff --git a/src/main/java/eu/europa/ted/eforms/sdk/entity/v2/SdkNoticeSubtypeV2.java b/src/main/java/eu/europa/ted/eforms/sdk/entity/v2/SdkNoticeSubtypeV2.java new file mode 100644 index 0000000..086a29c --- /dev/null +++ b/src/main/java/eu/europa/ted/eforms/sdk/entity/v2/SdkNoticeSubtypeV2.java @@ -0,0 +1,21 @@ +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.SdkNoticeSubtypeV1; + +/** + * Represents a notice subtype from the SDK's notice-types.json file. + */ +@SdkComponent(versions = {"2"}, componentType = SdkComponentType.NOTICE_TYPE) +public class SdkNoticeSubtypeV2 extends SdkNoticeSubtypeV1 { + + public SdkNoticeSubtypeV2(String subTypeId, String documentType, String type) { + super(subTypeId, documentType, type); + } + + public SdkNoticeSubtypeV2(JsonNode json) { + super(json); + } +} 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 1badd9b..11cdd54 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 @@ -39,7 +39,20 @@ protected MapFromJson(final String sdkVersion, final Path jsonPath) } } - private final void populateMap(final Path jsonPath) throws IOException, InstantiationException { + protected MapFromJson(final String sdkVersion, final Path jsonPath, final Object... context) + throws InstantiationException { + this.sdkVersion = sdkVersion; + + try { + populateMap(jsonPath, context); + } catch (IOException e) { + throw new RuntimeException(MessageFormat + .format("Failed to set resource filepath to [{0}]. Error was: {1}", jsonPath, e)); + } + } + + private final void populateMap(final Path jsonPath, final Object... context) + throws IOException, InstantiationException { logger.debug("Populating maps for context, jsonPath={}", jsonPath); final ObjectMapper mapper = buildStandardJacksonObjectMapper(); @@ -54,12 +67,24 @@ private final void populateMap(final Path jsonPath) throws IOException, Instanti } final JsonNode json = mapper.readTree(fieldsJsonInputStream); - populateMap(json); + populateMap(json, context); } } + /** + * Abstract method for populating the map from JSON. Existing subclasses implement this. + */ protected abstract void populateMap(final JsonNode json) throws InstantiationException; + /** + * Context-aware population method. Default implementation delegates to the abstract method, + * ignoring context. Subclasses that need context should override this method. + */ + protected void populateMap(final JsonNode json, final Object... context) + throws InstantiationException { + populateMap(json); + } + /** * @return A reusable Jackson object mapper instance. */ diff --git a/src/main/java/eu/europa/ted/eforms/sdk/repository/README.md b/src/main/java/eu/europa/ted/eforms/sdk/repository/README.md index ac456c0..598c7be 100644 --- a/src/main/java/eu/europa/ted/eforms/sdk/repository/README.md +++ b/src/main/java/eu/europa/ted/eforms/sdk/repository/README.md @@ -9,4 +9,6 @@ This package contains: * `SdkFieldRepository`: can populate a `HashMap` with `SdkField` objects read form `fields.json` * `SdkNodeRepository`: can populate a `HashMap` with `SdkNode` objects read form `fields.json` -* `SdkCodelistRepository`: can populate a `HashMap` with `SdkCodelist` objects (including all codelist codes), by reading the `.gc` files from the `codelists` folder of the eForms SDK. \ No newline at end of file +* `SdkCodelistRepository`: can populate a `HashMap` with `SdkCodelist` objects (including all codelist codes), by reading the `.gc` files from the `codelists` folder of the eForms SDK. +* `SdkNoticeTypeRepository`: can populate a `HashMap` with `SdkNoticeSubtype` objects read from `notice-types.json` +* `SdkDataTypeRepository`: can populate a `HashMap` with `SdkDataType` objects \ No newline at end of file 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 new file mode 100644 index 0000000..27cd9bd --- /dev/null +++ b/src/main/java/eu/europa/ted/eforms/sdk/repository/SdkDataTypeRepository.java @@ -0,0 +1,61 @@ +/* + * 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.HashMap; +import eu.europa.ted.eforms.sdk.entity.SdkDataType; + +/** + * Repository of SDK data types. + * + * Currently uses hardcoded type definitions. When data-types.json is added to the SDK, this class + * will be updated to load type metadata from JSON. + */ +public class SdkDataTypeRepository extends HashMap { + private static final long serialVersionUID = 1L; + + /** + * Creates a repository with the default set of SDK data types and their privacy masks. This is a + * temporary approach until data-types.json is available in the SDK. + */ + public static SdkDataTypeRepository createDefault() { + SdkDataTypeRepository repository = new SdkDataTypeRepository(); + + repository.addType("text", "unpublished"); + repository.addType("text-multilingual", "unpublished"); + repository.addType("code", "unpublished"); + repository.addType("internal-code", "unpublished"); + repository.addType("id", "unpublished"); + repository.addType("id-ref", "unpublished"); + repository.addType("phone", "unpublished"); + repository.addType("email", "unpublished"); + repository.addType("url", "unpublished"); + repository.addType("date", "1970-01-01Z"); + repository.addType("zoned-date", "1970-01-01Z"); + repository.addType("time", "00:00:00Z"); + repository.addType("zoned-time", "00:00:00Z"); + repository.addType("indicator", "0"); + repository.addType("integer", "-1"); + repository.addType("number", "-1"); + repository.addType("amount", "-1"); + repository.addType("measure", "-1"); + repository.addType("duration", "-1"); + + return repository; + } + + private void addType(String id, String privacyMask) { + this.put(id, new SdkDataType(id, privacyMask)); + } +} 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 e901fc9..a06f6aa 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 @@ -1,6 +1,8 @@ package eu.europa.ted.eforms.sdk.repository; import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ArrayNode; import eu.europa.ted.eforms.sdk.SdkConstants; @@ -14,12 +16,69 @@ public SdkFieldRepository(String sdkVersion, Path jsonPath) throws Instantiation super(sdkVersion, jsonPath); } + public SdkFieldRepository(String sdkVersion, Path jsonPath, SdkNodeRepository nodeRepository) + throws InstantiationException { + super(sdkVersion, jsonPath, nodeRepository); + } + @Override protected void populateMap(final JsonNode json) throws InstantiationException { + 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) + ? (SdkNodeRepository) context[0] + : null; + final ArrayNode fields = (ArrayNode) json.get(SdkConstants.FIELDS_JSON_FIELDS_KEY); + + // First pass: create all field entities and add them to the map for (final JsonNode field : fields) { final SdkField sdkField = SdkEntityFactory.getSdkField(sdkVersion, field); put(sdkField.getId(), sdkField); + + if (nodes != null && sdkField.getParentNodeId() != null) { + sdkField.setParentNode(nodes.get(sdkField.getParentNodeId())); + } + } + + // Second pass: resolve cross-field references + for (final SdkField sdkField : this.values()) { + if (sdkField.getPrivacySettings() != null) { + SdkField.PrivacySettings privacy = sdkField.getPrivacySettings(); + + if (privacy.getPrivacyCodeFieldId() != null) { + privacy.setPrivacyCodeField(this.get(privacy.getPrivacyCodeFieldId())); + } + if (privacy.getJustificationCodeFieldId() != null) { + privacy.setJustificationCodeField(this.get(privacy.getJustificationCodeFieldId())); + } + if (privacy.getJustificationDescriptionFieldId() != null) { + privacy.setJustificationDescriptionField( + this.get(privacy.getJustificationDescriptionFieldId())); + } + if (privacy.getPublicationDateFieldId() != null) { + privacy.setPublicationDateField(this.get(privacy.getPublicationDateFieldId())); + } + } + + if (!sdkField.getAttributes().isEmpty()) { + List attrFields = new ArrayList<>(); + for (String attrFieldId : sdkField.getAttributes()) { + SdkField attrField = this.get(attrFieldId); + if (attrField != null) { + attrFields.add(attrField); + } + } + sdkField.setAttributeFields(attrFields); + } + + if (sdkField.getAttributeOf() != null) { + sdkField.setAttributeOfField(this.get(sdkField.getAttributeOf())); + } } } } 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 adc2e56..c853153 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 @@ -1,6 +1,8 @@ package eu.europa.ted.eforms.sdk.repository; import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ArrayNode; import eu.europa.ted.eforms.sdk.SdkConstants; @@ -17,9 +19,26 @@ public SdkNodeRepository(String sdkVersion, Path jsonPath) throws InstantiationE @Override protected void populateMap(final JsonNode json) throws InstantiationException { final ArrayNode nodes = (ArrayNode) json.get(SdkConstants.FIELDS_JSON_XML_STRUCTURE_KEY); + List needsParentWiring = new ArrayList<>(); + + // 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); + + if (sdkNode.getParentId() != null) { + SdkNode parent = get(sdkNode.getParentId()); + if (parent != null) { + sdkNode.setParent(parent); + } else { + needsParentWiring.add(sdkNode); + } + } + } + + // Second pass: wire up any nodes whose parent wasn't loaded yet + for (SdkNode sdkNode : needsParentWiring) { + sdkNode.setParent(get(sdkNode.getParentId())); } } } diff --git a/src/main/java/eu/europa/ted/eforms/sdk/repository/SdkNoticeTypeRepository.java b/src/main/java/eu/europa/ted/eforms/sdk/repository/SdkNoticeTypeRepository.java new file mode 100644 index 0000000..adfdbba --- /dev/null +++ b/src/main/java/eu/europa/ted/eforms/sdk/repository/SdkNoticeTypeRepository.java @@ -0,0 +1,29 @@ +package eu.europa.ted.eforms.sdk.repository; + +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.entity.SdkEntityFactory; +import eu.europa.ted.eforms.sdk.entity.SdkNoticeSubtype; + +/** + * Repository for SDK notice types loaded from notice-types.json. + * Maps notice subtype IDs (e.g., "1", "3", "CEI", "E1", "X01") to SdkNoticeSubtype objects. + */ +public class SdkNoticeTypeRepository extends MapFromJson { + private static final long serialVersionUID = 1L; + + public SdkNoticeTypeRepository(String sdkVersion, Path jsonPath) throws InstantiationException { + super(sdkVersion, jsonPath); + } + + @Override + protected void populateMap(final JsonNode json) throws InstantiationException { + final ArrayNode noticeSubtypes = (ArrayNode) json.get(SdkConstants.NOTICE_TYPES_JSON_SUBTYPES_KEY); + for (final JsonNode noticeSubtype : noticeSubtypes) { + final SdkNoticeSubtype sdkNoticeSubtype = SdkEntityFactory.getSdkNoticeSubtype(sdkVersion, noticeSubtype); + put(sdkNoticeSubtype.getId(), sdkNoticeSubtype); + } + } +} diff --git a/src/main/java/eu/europa/ted/util/SafeDocumentBuilder.java b/src/main/java/eu/europa/ted/util/SafeDocumentBuilder.java new file mode 100644 index 0000000..b70d385 --- /dev/null +++ b/src/main/java/eu/europa/ted/util/SafeDocumentBuilder.java @@ -0,0 +1,96 @@ +/* + * Copyright 2022 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.util; + +import javax.xml.XMLConstants; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Utility class for the creation of {@link DocumentBuilder} instances for XML parsing, using XXE + * prevention techniques as recommended by OWASP. + * + * @see OWASP + * XXE Prevention Cheat Sheet + */ +public class SafeDocumentBuilder { + + private static final Logger logger = LoggerFactory.getLogger(SafeDocumentBuilder.class); + + private SafeDocumentBuilder() { + throw new AssertionError("Utility class."); + } + + /** + * Creates a {@link DocumentBuilder} using XXE prevention techniques. Allows DOCTYPE declarations. + * + * @return A {@link DocumentBuilder} instance + * @throws ParserConfigurationException when the builder is configured with a feature that is + * unsupported by the XML processor + */ + public static DocumentBuilder buildSafeDocumentBuilderAllowDoctype() + throws ParserConfigurationException { + return buildSafeDocumentBuilder(false); + } + + /** + * Creates a {@link DocumentBuilder} using XXE prevention techniques. Raises a fatal error when a + * DOCTYPE declaration is found. + * + * @return A {@link DocumentBuilder} instance + * @throws ParserConfigurationException when the builder is configured with a feature that is + * unsupported by the XML processor + */ + public static DocumentBuilder buildSafeDocumentBuilderStrict() + throws ParserConfigurationException { + return buildSafeDocumentBuilder(true); + } + + private static DocumentBuilder buildSafeDocumentBuilder(final boolean disallowDoctypeDecl) + throws ParserConfigurationException { + final DocumentBuilderFactory dbf = DocumentBuilderFactory.newDefaultInstance(); + String feature = null; + try { + feature = "http://apache.org/xml/features/disallow-doctype-decl"; + dbf.setFeature(feature, disallowDoctypeDecl); + + feature = "http://xml.org/sax/features/external-general-entities"; + dbf.setFeature(feature, false); + + feature = "http://xml.org/sax/features/external-parameter-entities"; + dbf.setFeature(feature, false); + + feature = "http://apache.org/xml/features/nonvalidating/load-external-dtd"; + dbf.setFeature(feature, false); + + dbf.setXIncludeAware(false); + dbf.setExpandEntityReferences(false); + dbf.setValidating(false); + dbf.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true); + dbf.setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, ""); + dbf.setFeature("http://apache.org/xml/features/nonvalidating/load-dtd-grammar", false); + + return dbf.newDocumentBuilder(); + } catch (final ParserConfigurationException e) { + logger.info("Error: The feature '{}' is probably not supported by your XML processor.", + feature); + logger.debug("ParserConfigurationException was thrown:", e); + throw e; + } + } +} diff --git a/src/test/java/eu/europa/ted/eforms/sdk/component/SdkComponentFactoryTest.java b/src/test/java/eu/europa/ted/eforms/sdk/component/SdkComponentFactoryTest.java index 4153763..2fca295 100644 --- a/src/test/java/eu/europa/ted/eforms/sdk/component/SdkComponentFactoryTest.java +++ b/src/test/java/eu/europa/ted/eforms/sdk/component/SdkComponentFactoryTest.java @@ -44,7 +44,7 @@ void testComponentNotFound() throws InstantiationException { // No component for this type assertThrows(IllegalArgumentException.class, () -> - factory.getComponentImpl("1.0", SdkComponentType.CODELIST, TestComponent.class)); + factory.getComponentImpl("1.0", SdkComponentType.VALIDATOR_GENERATOR, TestComponent.class)); // No component for this qualifier assertThrows(IllegalArgumentException.class, () -> @@ -53,9 +53,5 @@ void testComponentNotFound() throws InstantiationException { // Only component for this version and type does not have a qualifier assertThrows(IllegalArgumentException.class, () -> factory.getComponentImpl("0.5", SdkComponentType.EFX_EXPRESSION_TRANSLATOR, "BAD", TestComponent.class)); - - // Only component for this version and type has a qualifier - assertThrows(IllegalArgumentException.class, () -> - factory.getComponentImpl("1.0", SdkComponentType.NODE, TestComponent.class)); } }