From 7856af0d84819a3effbfe817387b2e721ef0b54c Mon Sep 17 00:00:00 2001 From: bern-SOPTIM Date: Sat, 17 Jan 2026 22:00:47 +0100 Subject: [PATCH 1/4] GH-3: Allow any cim namespace URIs The CIMXML library now supports any cim namespace and a configurable mapping of cim namespace URIs to custom `CimProfile` implementations besides the existing `CimProfile16`, `CimProfile17` and `CimProfile18`. For this mapping, the `CimNamespaceFactoryRegistry` has been introduced. The static enum `CimVersion` has be removed and all usages have been replaced by the namespace URIs. Before, there was a fixed set of cim namespaces supported by the CIMXML library: "http://iec.ch/TC57/2013/CIM-schema-cim16#" -> `CimVersion.`; "http://iec.ch/TC57/CIM100#" -> `CimVersion.CIM_17`; "https://cim.ucaiug.io/ns#" -> `CimVersion.CIM_18`; anything else: `CimVersion.NO_CIM`; New public method CimProfileRegistry#getMatchingProfiles to find matching profile for a given set of `owlVerionIRIs`. --> this enables for generich JSON-LD export demonstrated in `TestConvertCimXmlToJsonLd` Upgraded to Jena 5.6.0 Using Apache Jena `GraphFactory.createGraphMem()` instead of specific graph implementations. This makes migration easier. Especially with the renamed graph implementation in Jena 6.0 . - added several code quality plugins - spotbugs-maven-plugin - maven-checkstyle-plugin - maven-pmd-plugin - jacoco-maven-plugin - license-maven-plugin - cyclonedx-maven-plugin (for SBOM) - using the Google coding conventions for Java (checkstyle) - configured IntelliJ for Google coding conventions for Java Refactored CIMXML handling: rename classes and methods for consistency, add code style configuration, and update .gitignore. Updated keyword for FileHeaderProfiles for compatibility with CGMES 3.0 and refactor URI handling in conversion. --- .gitignore | 2 + .idea/.gitignore | 23 + .idea/codeStyles/Project.xml | 604 ++++++++++++++++++ .idea/codeStyles/codeStyleConfig.xml | 5 + cimxml/README.md | 8 +- cimxml/google_checks.xml | 482 ++++++++++++++ cimxml/intellij-java-google-style.xml | 598 +++++++++++++++++ cimxml/pom.xml | 262 +++++++- cimxml/spotbugs-excludes.xml | 56 ++ .../opencgmes/cimxml/CimHeaderVocabulary.java | 46 +- .../soptim/opencgmes/cimxml/CimVersion.java | 66 -- .../cimxml/CimXmlDocumentContext.java | 47 +- .../opencgmes/cimxml/graph/CimGraph.java | 53 +- .../cimxml/graph/CimModelHeader.java | 201 +++--- .../graph/CimNamespaceFactoryRegistry.java | 93 +++ .../opencgmes/cimxml/graph/CimProfile.java | 243 +++---- .../opencgmes/cimxml/graph/CimProfile16.java | 250 ++++---- .../opencgmes/cimxml/graph/CimProfile17.java | 240 ++++--- .../opencgmes/cimxml/graph/CimProfile18.java | 75 ++- .../cimxml/graph/DisjointMultiUnion.java | 162 +++-- .../cimxml/graph/FastDeltaGraph.java | 297 +++++---- .../opencgmes/cimxml/parser/CimXmlParser.java | 183 +++--- .../cimxml/parser/ParserCIMXML_StAX_SR.java | 31 +- .../opencgmes/cimxml/parser/RdfXmlParser.java | 239 +++---- .../cimxml/parser/ReaderCIMXML_StAX_SR.java | 21 +- .../cimxml/parser/system/StreamCIMXML.java | 77 --- .../system/StreamCIMXMLToDatasetGraph.java | 161 ----- .../cimxml/parser/system/StreamCimXml.java | 81 +++ .../system/StreamCimXmlToDatasetGraph.java | 153 +++++ .../cimxml/rdfs/CimProfileRegistry.java | 197 +++--- .../cimxml/rdfs/CimProfileRegistryStd.java | 523 ++++++++------- .../cimxml/sparql/core/CimDatasetGraph.java | 288 +++++---- .../sparql/core/LinkedCimDatasetGraph.java | 389 ++++++----- .../cimxml/graph/TestCimModelHeader.java | 23 +- .../cimxml/graph/TestCimProfile16.java | 25 +- .../cimxml/graph/TestCimProfile17.java | 18 +- .../cimxml/graph/TestCimProfile18.java | 7 +- .../rdfs/CimProfileRegistryStdTest.java | 10 +- .../sparql/core/CimDatasetGraphTest.java | 10 +- .../parser/TestConvertCimXmlToJsonLd.java | 138 ++++ .../parser/TestParserCIMXMLConformity.java | 101 +-- .../parser/TestParserRDFXMLConformity.java | 15 +- 42 files changed, 4478 insertions(+), 2025 deletions(-) create mode 100644 .idea/.gitignore create mode 100644 .idea/codeStyles/Project.xml create mode 100644 .idea/codeStyles/codeStyleConfig.xml create mode 100644 cimxml/google_checks.xml create mode 100644 cimxml/intellij-java-google-style.xml create mode 100644 cimxml/spotbugs-excludes.xml delete mode 100644 cimxml/src/main/java/de/soptim/opencgmes/cimxml/CimVersion.java create mode 100644 cimxml/src/main/java/de/soptim/opencgmes/cimxml/graph/CimNamespaceFactoryRegistry.java delete mode 100644 cimxml/src/main/java/de/soptim/opencgmes/cimxml/parser/system/StreamCIMXML.java delete mode 100644 cimxml/src/main/java/de/soptim/opencgmes/cimxml/parser/system/StreamCIMXMLToDatasetGraph.java create mode 100644 cimxml/src/main/java/de/soptim/opencgmes/cimxml/parser/system/StreamCimXml.java create mode 100644 cimxml/src/main/java/de/soptim/opencgmes/cimxml/parser/system/StreamCimXmlToDatasetGraph.java create mode 100644 cimxml/src/test/java/de/soptim/opencgmes/parser/TestConvertCimXmlToJsonLd.java diff --git a/.gitignore b/.gitignore index 524f0963..e21d5558 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,5 @@ # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml hs_err_pid* replay_pid* +/cimxml/target/ +/.idea/.gitignore diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 00000000..22db6617 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,23 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml +/compiler.xml +/copilot.data.migration.agent.xml +/copilot.data.migration.ask.xml +/copilot.data.migration.ask2agent.xml +/copilot.data.migration.edit.xml +/encodings.xml +/google-java-format.xml +/IntelliLang.xml +/jarRepositories.xml +/jpa.xml +/kotlinc.xml +/misc.xml +/modules.xml +/OpenCGMES.iml +/vcs.xml diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml new file mode 100644 index 00000000..11b084a6 --- /dev/null +++ b/.idea/codeStyles/Project.xml @@ -0,0 +1,604 @@ + + + + + + + \ No newline at end of file diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml new file mode 100644 index 00000000..79ee123c --- /dev/null +++ b/.idea/codeStyles/codeStyleConfig.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/cimxml/README.md b/cimxml/README.md index 724c723f..23fd56ec 100644 --- a/cimxml/README.md +++ b/cimxml/README.md @@ -59,12 +59,12 @@ The OpenCGMES CIMXML module provides a specialized parser and data structures fo de.soptim.opencgmes cimxml - 1.0.0 + 1.1.0 ``` ### Gradle ```gradle -implementation 'de.soptim.opencgmes:cimxml:1.0.0' +implementation 'de.soptim.opencgmes:cimxml:1.1.0' ``` ## Quick Start @@ -260,9 +260,7 @@ try (QueryExecution qexec = QueryExecutionFactory.create(query, dataset)) { The module uses specialized graph implementations optimized for CIM data: -- `GraphMem2Roaring`: Roaring bitmap-based indexing for large graphs -- `IndexingStrategy.LAZY_PARALLEL`: Deferred parallel index construction -- `FastDeltaGraph`: Efficient difference application without materialization +- `FastDeltaGraph`: Efficient difference application of difference models without materialization ### Large File Handling diff --git a/cimxml/google_checks.xml b/cimxml/google_checks.xml new file mode 100644 index 00000000..4e45d4f8 --- /dev/null +++ b/cimxml/google_checks.xml @@ -0,0 +1,482 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/cimxml/intellij-java-google-style.xml b/cimxml/intellij-java-google-style.xml new file mode 100644 index 00000000..f3a6743e --- /dev/null +++ b/cimxml/intellij-java-google-style.xml @@ -0,0 +1,598 @@ + + + + + + diff --git a/cimxml/pom.xml b/cimxml/pom.xml index ee2f8fb4..41b1e124 100644 --- a/cimxml/pom.xml +++ b/cimxml/pom.xml @@ -27,7 +27,7 @@ de.soptim.opencgmes cimxml - 1.0.0-SNAPSHOT + 1.1.0-SNAPSHOT jar OpenCGMES - IEC61970-552 CIMXML @@ -59,31 +59,41 @@ 21 - 5.5.0 + 5.6.0 4.2.2 - 1.3.3 + 1.3.4 7.1.1 - 2.20.0 - 3.18.0 + 2.21.0 + 3.20.0 2.0.17 - 2.25.1 + 2.25.3 4.13.2 - 5.19.0 + 5.22.0 4.3.0 - 3.14.0 - 3.5.3 - 3.3.1 + 3.15.0 + 3.5.5 + 3.4.0 3.11.3 + + + 4.9.8.2 + 3.6.0 + 3.6.0 + 2.9.1 + 0.8.14 + 3.28.0 + 2.7.1 + 13.2.0 @@ -185,6 +195,11 @@ ${ver.plugin.compiler} ${java.version} + + -Xlint:all,-options + -Werror + -proc:none + @@ -225,6 +240,233 @@ -missing + + + + com.github.spotbugs + spotbugs-maven-plugin + ${ver.plugin.spotbugs-maven-plugin} + + Max + Low + true + ${project.basedir}/spotbugs-excludes.xml + + + + + check + + + + + + + + org.apache.maven.plugins + maven-checkstyle-plugin + ${ver.plugin.maven-checkstyle-plugin} + + + + com.puppycrawl.tools + checkstyle + ${ver.plugin.checkstyle} + + + + + google_checks.xml + true + true + warning + **/parser/ParserCIMXML_StAX_SR.java,**/parser/ReaderCIMXML_StAX_SR.java + + + + checkstyle + validate + + check + + + + + + + + org.apache.maven.plugins + maven-pmd-plugin + ${ver.plugin.maven-pmd-plugin} + + true + true + + true + + + + pmd-check + + check + cpd-check + + + + xml + + + + + + + + org.apache.maven.plugins + maven-jxr-plugin + ${ver.plugin.maven-jxr-plugin} + + + false + vc-server Source Cross Reference + vc-server Source Cross Reference + + + + generate-xref + process-sources + + jxr + + + + + + + + org.jacoco + jacoco-maven-plugin + ${ver.plugin.jacoco-maven-plugin} + + + prepare-agent + + prepare-agent + + + + report + verify + + report + + + + check + + check + + + + + BUNDLE + + + INSTRUCTION + COVEREDRATIO + 0.40 + + + BRANCH + COVEREDRATIO + 0.30 + + + + + + + + + + + + org.codehaus.mojo + license-maven-plugin + ${ver.plugin.license-maven-plugin} + + apache_v2 + false + false + + Apache License, Version 2.0 + MIT License + + + + + download-licenses + + download-licenses + + + + + + + + org.cyclonedx + cyclonedx-maven-plugin + ${ver.plugin.cyclonedx-maven-plugin} + + + package + + makeAggregateBom + + + + + application + 1.5 + true + true + true + true + true + false + false + true + all + bom + false + + + + + + + + org.apache.maven.plugins + maven-jxr-plugin + 3.6.0 + + + + + + + org.apache.maven.plugins + maven-pmd-plugin + ${ver.plugin.maven-pmd-plugin} + + true + + /rulesets/java/quickstart.xml + + + + + \ No newline at end of file diff --git a/cimxml/spotbugs-excludes.xml b/cimxml/spotbugs-excludes.xml new file mode 100644 index 00000000..17c298d6 --- /dev/null +++ b/cimxml/spotbugs-excludes.xml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/cimxml/src/main/java/de/soptim/opencgmes/cimxml/CimHeaderVocabulary.java b/cimxml/src/main/java/de/soptim/opencgmes/cimxml/CimHeaderVocabulary.java index f1764682..fef66c30 100644 --- a/cimxml/src/main/java/de/soptim/opencgmes/cimxml/CimHeaderVocabulary.java +++ b/cimxml/src/main/java/de/soptim/opencgmes/cimxml/CimHeaderVocabulary.java @@ -25,28 +25,34 @@ * Vocabulary for CIMXML header information in RDF. */ public final class CimHeaderVocabulary { - public final static String NS_MD = "http://iec.ch/TC57/61970-552/ModelDescription/1#"; - public final static String NS_DM = "http://iec.ch/TC57/61970-552/DifferenceModel/1#"; - public final static String CLASSNAME_FULL_MODEL = "FullModel"; - public final static String CLASSNAME_DIFFERENCE_MODEL = "DifferenceModel"; - public final static String TAG_NAME_FORWARD_DIFFERENCES = "forwardDifferences"; - public final static String TAG_NAME_REVERSE_DIFFERENCES = "reverseDifferences"; - public final static String TAG_NAME_PRECONDITIONS = "preconditions"; - public final static Node PREDICATE_PROFILE = NodeFactory.createURI(NS_MD + "Model.profile"); - public final static Node PREDICATE_SUPERSEDES = NodeFactory.createURI(NS_MD + "Model.Supersedes"); - public final static Node PREDICATE_DEPENDENT_ON = NodeFactory.createURI(NS_MD + "Model.DependentOn"); + public static final String NS_MD = "http://iec.ch/TC57/61970-552/ModelDescription/1#"; + public static final String NS_DM = "http://iec.ch/TC57/61970-552/DifferenceModel/1#"; + public static final String CLASSNAME_FULL_MODEL = "FullModel"; + public static final String CLASSNAME_DIFFERENCE_MODEL = "DifferenceModel"; + public static final String TAG_NAME_FORWARD_DIFFERENCES = "forwardDifferences"; + public static final String TAG_NAME_REVERSE_DIFFERENCES = "reverseDifferences"; + public static final String TAG_NAME_PRECONDITIONS = "preconditions"; - public final static String FULL_MODEL_URI = NS_MD + CLASSNAME_FULL_MODEL; - public final static String TYPE_DIFFERENCE_MODEL_URI = NS_DM + CLASSNAME_DIFFERENCE_MODEL; - public final static String GRAPH_FORWARD_DIFFERENCES_URI = NS_DM + TAG_NAME_FORWARD_DIFFERENCES; - public final static String GRAPH_REVERSE_DIFFERENCES_URI = NS_DM + TAG_NAME_REVERSE_DIFFERENCES; - public final static String GRAPH_PRECONDITIONS_URI = NS_DM + TAG_NAME_PRECONDITIONS; + public static final Node PREDICATE_PROFILE = NodeFactory.createURI(NS_MD + "Model.profile"); + public static final Node PREDICATE_SUPERSEDES = NodeFactory.createURI(NS_MD + "Model.Supersedes"); + public static final Node PREDICATE_DEPENDENT_ON + = NodeFactory.createURI(NS_MD + "Model.DependentOn"); - public final static Node TYPE_FULL_MODEL = NodeFactory.createURI(FULL_MODEL_URI); - public final static Node TYPE_DIFFERENCE_MODEL = NodeFactory.createURI(TYPE_DIFFERENCE_MODEL_URI); + public static final String FULL_MODEL_URI = NS_MD + CLASSNAME_FULL_MODEL; + public static final Node TYPE_FULL_MODEL = NodeFactory.createURI(FULL_MODEL_URI); + public static final String TYPE_DIFFERENCE_MODEL_URI = NS_DM + CLASSNAME_DIFFERENCE_MODEL; + public static final Node TYPE_DIFFERENCE_MODEL = NodeFactory.createURI(TYPE_DIFFERENCE_MODEL_URI); + public static final String GRAPH_FORWARD_DIFFERENCES_URI = NS_DM + TAG_NAME_FORWARD_DIFFERENCES; + public static final Node GRAPH_FORWARD_DIFFERENCES + = NodeFactory.createURI(GRAPH_FORWARD_DIFFERENCES_URI); + public static final String GRAPH_REVERSE_DIFFERENCES_URI = NS_DM + TAG_NAME_REVERSE_DIFFERENCES; + public static final Node GRAPH_REVERSE_DIFFERENCES + = NodeFactory.createURI(GRAPH_REVERSE_DIFFERENCES_URI); + public static final String GRAPH_PRECONDITIONS_URI = NS_DM + TAG_NAME_PRECONDITIONS; + public static final Node GRAPH_PRECONDITIONS = NodeFactory.createURI(GRAPH_PRECONDITIONS_URI); - public final static Node GRAPH_FORWARD_DIFFERENCES = NodeFactory.createURI(GRAPH_FORWARD_DIFFERENCES_URI); - public final static Node GRAPH_REVERSE_DIFFERENCES = NodeFactory.createURI(GRAPH_REVERSE_DIFFERENCES_URI); - public final static Node GRAPH_PRECONDITIONS = NodeFactory.createURI(GRAPH_PRECONDITIONS_URI); + private CimHeaderVocabulary() { + // prevent instantiation + } } diff --git a/cimxml/src/main/java/de/soptim/opencgmes/cimxml/CimVersion.java b/cimxml/src/main/java/de/soptim/opencgmes/cimxml/CimVersion.java deleted file mode 100644 index c089572a..00000000 --- a/cimxml/src/main/java/de/soptim/opencgmes/cimxml/CimVersion.java +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package de.soptim.opencgmes.cimxml; - -import java.util.Objects; - -/** - * Enumeration of CIM versions known to this library. - *

- * The CIM version is identified by the namespace prefix URI used in the RDF representation of CIM. - *

- * See also CIM 16, - * CIM 17, and - * CIM 18. - */ -public enum CimVersion { - /** No CIM version specified */ - NO_CIM, - /** - * CIM version 16. - * This version is used in CGMES v2.4.15 - */ - CIM_16, - /** - * CIM version 17. - * This version is used in CGMES v3.0 - * */ - CIM_17, - /** - * CIM version 18. - * There is no matching version of CGMES yet. - */ - CIM_18; - - /** - * Get the CIM version for a given CIM namespace. - * - * @param namespace The CIM namespace prefix URI. Must not be {@code null}. - * @return The corresponding CIM version, or {@link #NO_CIM} if the namespace is not recognized. - * @throws NullPointerException if {@code namespace} is {@code null}. - */ - public static CimVersion fromCimNamespace(String namespace) { - Objects.requireNonNull(namespace, "namespace"); - return switch(namespace) { - case "http://iec.ch/TC57/2013/CIM-schema-cim16#" -> CimVersion.CIM_16; - case "http://iec.ch/TC57/CIM100#" -> CimVersion.CIM_17; - case "https://cim.ucaiug.io/ns#" -> CimVersion.CIM_18; - default -> CimVersion.NO_CIM; - }; - } -} diff --git a/cimxml/src/main/java/de/soptim/opencgmes/cimxml/CimXmlDocumentContext.java b/cimxml/src/main/java/de/soptim/opencgmes/cimxml/CimXmlDocumentContext.java index 3402a495..5dd77246 100644 --- a/cimxml/src/main/java/de/soptim/opencgmes/cimxml/CimXmlDocumentContext.java +++ b/cimxml/src/main/java/de/soptim/opencgmes/cimxml/CimXmlDocumentContext.java @@ -22,30 +22,31 @@ import org.apache.jena.sparql.core.Quad; /** - * The context of a CIMXML document: full model, difference model, or one of the named graphs - * in a difference model. + * The context of a CIMXML document: full model, difference model, or one of the named graphs in a + * difference model. */ public enum CimXmlDocumentContext { - fullModel, - body, - differenceModel, - forwardDifferences, - reverseDifferences, - preconditions; + fullModel, + body, + differenceModel, + forwardDifferences, + reverseDifferences, + preconditions; - /** - * Get the graph name (Node) for the given context. - * @param context the context - * @return the graph name (Node) - */ - public static Node getGraphName(CimXmlDocumentContext context) { - return switch (context) { - case fullModel -> CimHeaderVocabulary.TYPE_FULL_MODEL; - case body -> Quad.defaultGraphIRI; - case differenceModel -> CimHeaderVocabulary.TYPE_DIFFERENCE_MODEL; - case forwardDifferences -> CimHeaderVocabulary.GRAPH_FORWARD_DIFFERENCES; - case reverseDifferences -> CimHeaderVocabulary.GRAPH_REVERSE_DIFFERENCES; - case preconditions -> CimHeaderVocabulary.GRAPH_PRECONDITIONS; - }; - } + /** + * Get the graph name (Node) for the given context. + * + * @param context the context + * @return the graph name (Node) + */ + public static Node getGraphName(CimXmlDocumentContext context) { + return switch (context) { + case fullModel -> CimHeaderVocabulary.TYPE_FULL_MODEL; + case body -> Quad.defaultGraphIRI; + case differenceModel -> CimHeaderVocabulary.TYPE_DIFFERENCE_MODEL; + case forwardDifferences -> CimHeaderVocabulary.GRAPH_FORWARD_DIFFERENCES; + case reverseDifferences -> CimHeaderVocabulary.GRAPH_REVERSE_DIFFERENCES; + case preconditions -> CimHeaderVocabulary.GRAPH_PRECONDITIONS; + }; + } } diff --git a/cimxml/src/main/java/de/soptim/opencgmes/cimxml/graph/CimGraph.java b/cimxml/src/main/java/de/soptim/opencgmes/cimxml/graph/CimGraph.java index c634c376..c2bde564 100644 --- a/cimxml/src/main/java/de/soptim/opencgmes/cimxml/graph/CimGraph.java +++ b/cimxml/src/main/java/de/soptim/opencgmes/cimxml/graph/CimGraph.java @@ -18,39 +18,36 @@ package de.soptim.opencgmes.cimxml.graph; -import de.soptim.opencgmes.cimxml.CimVersion; -import org.apache.jena.graph.Graph; - import java.util.Objects; +import org.apache.jena.graph.Graph; /** - * A specialization of {@link Graph} that provides methods to determine the CIM version - * of the graph based on its namespace prefixes. + * A specialization of {@link Graph} that provides methods to determine the 'cim' namespace of the + * graph based on its namespace prefixes. */ public interface CimGraph extends Graph { - /** - * Determines the CIM version of this graph based on its namespace prefixes. - * If the graph does not use a CIM namespace, {@link CimVersion#NO_CIM} is returned. - * @return The CIM version of the graph, or {@link CimVersion#NO_CIM} if no CIM namespace is used. - */ - default CimVersion getCIMVersion() { - return getCIMXMLVersion(this); - } - - /** - * Determines the CIM version of the given graph based on its namespace prefixes. - * If the graph does not use a CIM namespace, {@link CimVersion#NO_CIM} is returned. - * @param graph The graph to determine the CIM version for. Must not be null. - * @return The CIM version of the graph, or {@link CimVersion#NO_CIM} if no CIM namespace is used. - * @throws NullPointerException if the graph is null. - */ - static CimVersion getCIMXMLVersion(Graph graph) { - Objects.requireNonNull(graph, "graph"); - var cimURI = graph.getPrefixMapping().getNsPrefixURI("cim"); - if (cimURI == null) - return CimVersion.NO_CIM; + /** + * Get the CIM namespace URI associated with the 'cim' prefix in the given graph's prefix + * mapping. + * + * @param graph the graph from which to retrieve the CIM namespace + * @return the CIM namespace URI or null if the 'cim' prefix is not defined in the graph's prefix + * mapping + * @throws NullPointerException if the graph is null + */ + static String getCimNs(Graph graph) { + Objects.requireNonNull(graph, "graph"); + return graph.getPrefixMapping().getNsPrefixURI("cim"); + } - return CimVersion.fromCimNamespace(graph.getPrefixMapping().getNsPrefixURI("cim")); - } + /** + * Get the CIM namespace URI associated with the 'cim' prefix in the graph's prefix mapping. + * + * @return the CIM namespace URI or null if the 'cim' prefix is not defined in the graph's prefix + * mapping + */ + default String getCimNamespace() { + return getCimNs(this); + } } diff --git a/cimxml/src/main/java/de/soptim/opencgmes/cimxml/graph/CimModelHeader.java b/cimxml/src/main/java/de/soptim/opencgmes/cimxml/graph/CimModelHeader.java index d9871841..1fa87b2e 100644 --- a/cimxml/src/main/java/de/soptim/opencgmes/cimxml/graph/CimModelHeader.java +++ b/cimxml/src/main/java/de/soptim/opencgmes/cimxml/graph/CimModelHeader.java @@ -19,121 +19,130 @@ package de.soptim.opencgmes.cimxml.graph; import de.soptim.opencgmes.cimxml.CimHeaderVocabulary; -import de.soptim.opencgmes.cimxml.CimVersion; +import java.util.Set; +import java.util.stream.Collectors; import org.apache.jena.graph.Graph; import org.apache.jena.graph.Node; import org.apache.jena.graph.Triple; import org.apache.jena.sparql.graph.GraphWrapper; import org.apache.jena.vocabulary.RDF; -import java.util.Set; -import java.util.stream.Collectors; - /** * A wrapper for a graph that contains the model header information of a CIMXML document. - *

- * The model header is expected to contain exactly one instance of either md:FullModel or dm:DifferenceModel. - * It may contain zero or more "Model.profile" references, each referencing one of the registered profile ontologies. - * It may also contain zero or more Model.Supersedes and Model.DependentOn references to other models. + * + *

The model header is expected to contain exactly one instance of either md:FullModel or + * dm:DifferenceModel. It may contain zero or more "Model.profile" references, each referencing one + * of the registered profile ontologies. It may also contain zero or more Model.Supersedes and + * Model.DependentOn references to other models. */ public interface CimModelHeader extends CimGraph { - /** - * Checks if the model is a full model. - * @return true if the model is a full model, false otherwise. - */ - default boolean isFullModel() { - return find(Node.ANY, RDF.type.asNode(), CimHeaderVocabulary.TYPE_FULL_MODEL).hasNext(); + /** + * Wraps a given graph as a {@link CimModelHeader}. If the graph is already an instance of + * {@link CimModelHeader}, it is returned as is. If the graph is null, null is returned. If the + * graph does not appear to be a CIM graph (no 'cim' namespace defined), an + * IllegalArgumentException is thrown. + * + * @param graph The graph to wrap. + * @return The wrapped graph as a {@link CimModelHeader}, or null if the input graph is null. + * @throws IllegalArgumentException if the graph does not appear to be a CIM graph. + */ + static CimModelHeader wrap(Graph graph) { + if (graph == null) { + return null; } - - /** - * Checks if the model is a difference model. - * @return true if the model is a difference model, false otherwise. - */ - default boolean isDifferenceModel() { - return find(Node.ANY, RDF.type.asNode(), CimHeaderVocabulary.TYPE_DIFFERENCE_MODEL).hasNext(); + if (graph instanceof CimModelHeader cimModelHeader) { + return cimModelHeader; } - - /** - * Get the node representing the model (either md:FullModel or dm:DifferenceModel). - * The node is expected to be an IRI. - * @return The model node. - * @throws IllegalStateException if neither a FullModel nor a DifferenceModel is found in the header graph. - */ - default Node getModel() { - var iter = find(Node.ANY, RDF.type.asNode(), CimHeaderVocabulary.TYPE_FULL_MODEL); - if (iter.hasNext()) { - return iter.next().getSubject(); - } - iter = find(Node.ANY, RDF.type.asNode(), CimHeaderVocabulary.TYPE_DIFFERENCE_MODEL); - if (iter.hasNext()) { - return iter.next().getSubject(); - } - throw new IllegalStateException("Found neither FullModel nor DifferenceModel in the header graph."); + if (CimGraph.getCimNs(graph) == null) { + throw new IllegalArgumentException( + "Graph does not appear to be a CIM graph. No proper 'cim' namespace defined."); } + return new CimModelHeaderGraphWrapper(graph); + } - /** - * Get the profiles associated with the model. - * Each profile node in a model header references one owlVersionIRI in {@link CimProfile#getOwlVersionIRIs()} - * of the matching profile ontology. - * @return A set of profile nodes (IRIs). - */ - default Set getProfiles() { - return stream(getModel(), CimHeaderVocabulary.PREDICATE_PROFILE, Node.ANY) - .map(Triple::getObject) - .collect(Collectors.toUnmodifiableSet()); - } + /** + * Checks if the model is a full model. + * + * @return true if the model is a full model, false otherwise. + */ + default boolean isFullModel() { + return find(Node.ANY, RDF.type.asNode(), CimHeaderVocabulary.TYPE_FULL_MODEL).hasNext(); + } - /** - * Get the models that are superseded by this model. - * Each superseded model is referenced by its IRI. - * @return A set of model IRIs that are superseded by this model. - */ - default Set getSupersedes() { - return stream(getModel(), CimHeaderVocabulary.PREDICATE_SUPERSEDES, Node.ANY) - .map(Triple::getObject) - .collect(Collectors.toUnmodifiableSet()); - } + /** + * Checks if the model is a difference model. + * + * @return true if the model is a difference model, false otherwise. + */ + default boolean isDifferenceModel() { + return find(Node.ANY, RDF.type.asNode(), CimHeaderVocabulary.TYPE_DIFFERENCE_MODEL).hasNext(); + } - /** - * Gets all models that this model is dependent on. - * Each dependent model is referenced by its IRI. - * @return A set of models (IRIs) that this model is dependent on. - */ - default Set getDependentOn() { - return stream(getModel(), CimHeaderVocabulary.PREDICATE_DEPENDENT_ON, Node.ANY) - .map(Triple::getObject) - .collect(Collectors.toUnmodifiableSet()); + /** + * Get the node representing the model (either md:FullModel or dm:DifferenceModel). The node is + * expected to be an IRI. + * + * @return The model node. + * @throws IllegalStateException if neither a FullModel nor a DifferenceModel is found in the + * header graph. + */ + default Node getModel() { + var iter = find(Node.ANY, RDF.type.asNode(), CimHeaderVocabulary.TYPE_FULL_MODEL); + if (iter.hasNext()) { + return iter.next().getSubject(); } - - /** - * Wraps a given graph as a {@link CimModelHeader}. - * If the graph is already an instance of {@link CimModelHeader}, it is returned as is. - * If the graph is null, null is returned. - * If the graph does not appear to be a CIM graph (no 'cim' namespace defined), an IllegalArgumentException is thrown. - * @param graph The graph to wrap. - * @return The wrapped graph as a {@link CimModelHeader}, or null if the input graph is null. - * @throws IllegalArgumentException if the graph does not appear to be a CIM graph. - */ - static CimModelHeader wrap(Graph graph) { - if (graph == null) { - return null; - } - if (graph instanceof CimModelHeader cimModelHeader) { - return cimModelHeader; - } - if (CimGraph.getCIMXMLVersion(graph) == CimVersion.NO_CIM) { - throw new IllegalArgumentException("Graph does not appear to be a CIM graph. No proper 'cim' namespace defined."); - } - return new CimModelHeaderGraphWrapper(graph); + iter = find(Node.ANY, RDF.type.asNode(), CimHeaderVocabulary.TYPE_DIFFERENCE_MODEL); + if (iter.hasNext()) { + return iter.next().getSubject(); } + throw new IllegalStateException( + "Found neither FullModel nor DifferenceModel in the header graph."); + } + + /** + * Get the profiles associated with the model. Each profile node in a model header references one + * owlVersionIRI in {@link CimProfile#getOwlVersionIris()} of the matching profile ontology. + * + * @return A set of profile nodes (IRIs). + */ + default Set getProfiles() { + return stream(getModel(), CimHeaderVocabulary.PREDICATE_PROFILE, Node.ANY) + .map(Triple::getObject) + .collect(Collectors.toUnmodifiableSet()); + } + + /** + * Get the models that are superseded by this model. Each superseded model is referenced by its + * IRI. + * + * @return A set of model IRIs that are superseded by this model. + */ + default Set getSupersedes() { + return stream(getModel(), CimHeaderVocabulary.PREDICATE_SUPERSEDES, Node.ANY) + .map(Triple::getObject) + .collect(Collectors.toUnmodifiableSet()); + } + + /** + * Gets all models that this model is dependent on. Each dependent model is referenced by its + * IRI. + * + * @return A set of models (IRIs) that this model is dependent on. + */ + default Set getDependentOn() { + return stream(getModel(), CimHeaderVocabulary.PREDICATE_DEPENDENT_ON, Node.ANY) + .map(Triple::getObject) + .collect(Collectors.toUnmodifiableSet()); + } + + /** + * An implementation of {@link CimModelHeader} that wraps a {@link Graph}. + */ + class CimModelHeaderGraphWrapper extends GraphWrapper implements CimModelHeader { - /** - * An implementation of {@link CimModelHeader} that wraps a {@link Graph}. - */ - class CimModelHeaderGraphWrapper extends GraphWrapper implements CimModelHeader { - public CimModelHeaderGraphWrapper(Graph graph) { - super(graph); - } + public CimModelHeaderGraphWrapper(Graph graph) { + super(graph); } + } } diff --git a/cimxml/src/main/java/de/soptim/opencgmes/cimxml/graph/CimNamespaceFactoryRegistry.java b/cimxml/src/main/java/de/soptim/opencgmes/cimxml/graph/CimNamespaceFactoryRegistry.java new file mode 100644 index 00000000..56d3e34c --- /dev/null +++ b/cimxml/src/main/java/de/soptim/opencgmes/cimxml/graph/CimNamespaceFactoryRegistry.java @@ -0,0 +1,93 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package de.soptim.opencgmes.cimxml.graph; + +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Function; +import org.apache.jena.graph.Graph; + +/** + * A registry for mapping CIM namespaces to factory functions that create {@link CimProfile} + * instances for those namespaces. This allows for dynamic registration and retrieval of profile + * factories based on the CIM namespace of a graph. + */ +public class CimNamespaceFactoryRegistry { + + public static final String NAMESPACE = "namespace"; + private static final Map> map; + + static { + map = new ConcurrentHashMap<>(); + map.put(CimProfile16.CIM_NAMESPACE, CimProfile16::new); + map.put(CimProfile17.CIM_NAMESPACE, CimProfile17::new); + map.put(CimProfile18.CIM_NAMESPACE, CimProfile18::new); + } + + /** + * Registers a factory function for creating {@link CimProfile} instances for the given + * CIM namespace. + * + * @param namespace the CIM namespace for which the factory should be registered + * @param factory a function that takes a {@link Graph} and returns a {@link CimProfile} + * @throws NullPointerException if either the namespace or the factory is null + */ + public static void registerProfileFactory(String namespace, Function factory) { + Objects.requireNonNull(namespace, NAMESPACE); + Objects.requireNonNull(factory, "constructor"); + map.put(namespace, factory); + } + + /** + * Unregisters the factory function associated with the given CIM namespace. + * + * @param namespace the CIM namespace for which the factory should be unregistered + * @throws NullPointerException if the namespace is null + */ + public static void unregisterProfileFactory(String namespace) { + Objects.requireNonNull(namespace, NAMESPACE); + map.remove(namespace); + } + + /** + * Retrieves the factory function associated with the given CIM namespace. + * + * @param namespace the CIM namespace for which to retrieve the factory + * @return a function that takes a {@link Graph} and returns a {@link CimProfile}, + * or null if no factory is registered for the given namespace + * @throws NullPointerException if the namespace is null + */ + public static Function getProfileFactory(String namespace) { + Objects.requireNonNull(namespace, NAMESPACE); + return map.get(namespace); + } + + /** + * Checks if a factory function is registered for the given CIM namespace. + * + * @param namespace the CIM namespace to check + * @return true if a factory function is registered for the given namespace, false otherwise + * @throws NullPointerException if the namespace is null + */ + public static boolean hasProfileFactory(String namespace) { + Objects.requireNonNull(namespace, NAMESPACE); + return map.containsKey(namespace); + } +} diff --git a/cimxml/src/main/java/de/soptim/opencgmes/cimxml/graph/CimProfile.java b/cimxml/src/main/java/de/soptim/opencgmes/cimxml/graph/CimProfile.java index dcb63ef2..07c77685 100644 --- a/cimxml/src/main/java/de/soptim/opencgmes/cimxml/graph/CimProfile.java +++ b/cimxml/src/main/java/de/soptim/opencgmes/cimxml/graph/CimProfile.java @@ -18,22 +18,21 @@ package de.soptim.opencgmes.cimxml.graph; -import de.soptim.opencgmes.cimxml.CimVersion; import de.soptim.opencgmes.cimxml.rdfs.CimProfileRegistry; +import java.util.Set; import org.apache.jena.graph.Graph; import org.apache.jena.graph.Node; import org.apache.jena.graph.NodeFactory; -import java.util.Set; - /** * Represents a CIM profile ontology (RDFS schema) graph. * *

A CIM profile defines a subset of the CIM schema for specific use cases, such as - * equipment models, topology, or state variables. Profiles are versioned and identified - * by their version IRIs.

+ * equipment models, topology, or state variables. Profiles are versioned and identified by their + * version IRIs.

* *

Profile Structure:

+ * *

CIM profiles can be defined in different formats depending on the CIM version:

* *

CIM 16 (CGMES 2.4.15):

@@ -57,158 +56,126 @@ * CimProfile profile = CimProfile.wrap(profileGraph); * * // Query profile metadata - * CimVersion version = profile.getCIMVersion(); + * String cimNamespace = profile.getCimNamespace(); * Set versionIris = profile.getOwlVersionIRIs(); * String keyword = profile.getDcatKeyword(); * boolean isHeader = profile.isHeaderProfile(); * } * - * @see CimVersion * @see CimProfileRegistry * @since Jena 5.6.0 */ public interface CimProfile extends CimGraph { - String NS_CIMS = "http://iec.ch/TC57/1999/rdf-schema-extensions-19990926#"; - String CLASS_CLASS_CATEGORY = "ClassCategory"; - String PACKAGE_FILE_HEADER_PROFILE = "#Package_FileHeaderProfile"; + String NS_CIMS = "http://iec.ch/TC57/1999/rdf-schema-extensions-19990926#"; + String CLASS_CLASS_CATEGORY = "ClassCategory"; + String PACKAGE_FILE_HEADER_PROFILE = "#Package_FileHeaderProfile"; - Node TYPE_CLASS_CATEGORY = NodeFactory.createURI(NS_CIMS + CLASS_CLASS_CATEGORY); + Node TYPE_CLASS_CATEGORY = NodeFactory.createURI(NS_CIMS + CLASS_CLASS_CATEGORY); - /** - * The header profile describes the RDF schema for a CIM model header or document header. - * These profiles are not references by the model. So in the profile registry header profiles usually are not - * selected by their versionIRI. - * - * @return true if this profile is a header profile, false otherwise. - */ - boolean isHeaderProfile(); + /** + * Wraps a graph as a CimProfile. If the graph is already a CimProfile, it is returned as is. + * Otherwise, a new ProfileOntologyImpl is created wrapping the graph. + * + *

If the graph does not appear to be a CIM graph (no 'cim' namespace defined), an + * IllegalArgumentException is thrown. + * + * @param graph The graph to wrap. + * @return A CimProfile wrapping the given graph. + * @throws IllegalArgumentException if the graph does not appear to be a CIM graph. + */ + static CimProfile wrap(Graph graph) throws IllegalArgumentException { + if (graph instanceof CimProfile po) { + return po; + } + var cimNamespace = CimGraph.getCimNs(graph); + if (cimNamespace == null) { + throw new IllegalArgumentException( + "Graph does not appear to be a CIM graph. No proper 'cim' namespace defined."); + } + var factory = CimNamespaceFactoryRegistry.getProfileFactory(cimNamespace); + if (factory == null) { + throw new IllegalArgumentException( + "No profile factory registered for 'cim' namespace URI: " + cimNamespace); + } + return factory.apply(graph); + } - /** - * Abbreviation or keyword for the profile. - * This is usually dcat:keyword. - * For CGMES 2.4.15 profiles it is "{Profile}Version.shortName cims:isFixed ?keyword". - *

- * In CGMES 2.4.15 file header profiles do not contain a "shortName" or "keyword". But the new ontology document - * header typically contain "DH" as keyword. To maintain compatibility, "DH" shall be used for old CGMES 2.4.15 - * file header profiles. - * - * @return The keyword for the profile, or null if no keyword is defined. - */ - String getDcatKeyword(); + /** + * The header profile describes the RDF schema for a CIM model header or document header. These + * profiles are not references by the model. So in the profile registry header profiles usually + * are not selected by their versionIRI. + * + * @return true if this profile is a header profile, false otherwise. + */ + boolean isHeaderProfile(); - /** - * The version IRIs of the profile. - * This is usually owl:versionIRI. - * For CGMES 2.4.15 profiles it is - * "{Profile}Version.baseURI.{*} cims:isFixed ?versionIRI" - * and - * "{Profile}Version.entsoeURI{*} cims:isFixed ?versionIRI". - *

- * One profile can have multiple version IRIs, at least in CGMES 2.4.15 profiles. - * - * @return The version IRI of the profile, or null if no version IRI is defined. - */ - Set getOwlVersionIRIs(); + /** + * Abbreviation or keyword for the profile. This is usually dcat:keyword. For CGMES 2.4.15 + * profiles it is "{Profile}Version.shortName cims:isFixed ?keyword". + * + *

In CGMES 2.4.15 file header profiles do not contain a "shortName" or "keyword". But the new + * ontology document header typically contain "FH" as keyword. To maintain compatibility, "FH" + * shall be used for old CGMES 2.4.15 file header profiles. + * + * @return The keyword for the profile, or null if no keyword is defined. + */ + String getDcatKeyword(); - /** - * Return owl:versionInfo of the ontology object of the profile. - * For CGMES 2.4.15, there is no such version info. - * - * @return The version info of the profile, or null if no version info is defined. - */ - String getOwlVersionInfo(); + /** + * The version IRIs of the profile. This is usually owl:versionIRI. For CGMES 2.4.15 profiles it + * is "{Profile}Version.baseURI.{*} cims:isFixed ?versionIRI" and "{Profile}Version.entsoeURI{*} + * cims:isFixed ?versionIRI". + * + *

One profile can have multiple version IRIs, at least in CGMES 2.4.15 profiles. + * + * @return The version IRI of the profile, or null if no version IRI is defined. + */ + Set getOwlVersionIris(); - /** - * Checks if this profile is equal to another profile. - * Two profiles are equal if they have the same CIM version and the same set of version - * IRIs, or if both are header profiles. - * @param other The other profile to compare to. - * @return true if the profiles are equal, false otherwise. - */ - default boolean equals(CimProfile other) { - if (other == null) { - return false; - } - if (!this.getCIMVersion().equals(other.getCIMVersion())) { - return false; - } - if (isHeaderProfile()) { - return other.isHeaderProfile(); - } - return this.getOwlVersionIRIs().equals(other.getOwlVersionIRIs()); - } + /** + * Return owl:versionInfo of the ontology object of the profile. For CGMES 2.4.15, there is no + * such version info. + * + * @return The version info of the profile, or null if no version info is defined. + */ + String getOwlVersionInfo(); - /** - * Calculates the hash code for this profile. - * If the model is a header profile, the hash code is based on the CIM version and the fact that it is a header - * profile. - * If the model is not a header profile, the hash code is based on the CIM version and the set of version IRIs. - * @return The hash code for this profile. - */ - default int calculateHashCode() { - // hash code from isHeader, cimVersion and version IRIs - int result = Boolean.hashCode(isHeaderProfile()); - result = 31 * result + getCIMVersion().hashCode(); - if (!isHeaderProfile()) { - result = 31 * result + getOwlVersionIRIs().hashCode(); - } - return result; + /** + * Checks if this profile is equal to another profile. Two profiles are equal if they have the + * same CIM version and the same set of version IRIs, or if both are header profiles. + * + * @param other The other profile to compare to. + * @return true if the profiles are equal, false otherwise. + */ + default boolean equals(CimProfile other) { + if (other == null) { + return false; } - - /** - * Wraps a graph as a CimProfile. - * If the graph is already a CimProfile, it is returned as is. - * Otherwise, a new ProfileOntologyImpl is created wrapping the graph. - * - * @param graph The graph to wrap. - * @return A CimProfile wrapping the given graph. - */ - static CimProfile wrap(Graph graph) throws IllegalArgumentException { - if (graph instanceof CimProfile po) { - return po; - } - var cimVersion = CimGraph.getCIMXMLVersion(graph); - return switch (cimVersion) { - case CIM_16 -> { - if (CimProfile16.isHeaderProfile(graph)) { - // If the graph contains header profile, skip the version IRI and keyword check. - yield new CimProfile16(graph, true); - } - if (CimProfile16.hasVersionIRIAndKeyword(graph)) { - yield new CimProfile16(graph, false); - } - throw new IllegalArgumentException("Graph does not contain the required '...Version.shortName' and '...Version.entsoeURI*' or '...Version.baseURI...' for a CGMES 2.4.15 profile."); - } - case CIM_17 -> { - if (CimProfile17.isHeaderProfile(graph)) { - // If the graph contains header profile --> it is still CIM16 style - yield new CimProfile17(graph, true); - } - if (!CimProfile17.hasOntology(graph)) { - throw new IllegalArgumentException("Graph does not contain the required ontology subject for a CIM profile."); - } - if (!CimProfile17.hasVersionIRIAndKeyword(graph)) { - throw new IllegalArgumentException("Graphs ontology does not contain the required versionIRI and keyword for a CIM profile."); - } - yield new CimProfile17(graph, false); - } - case CIM_18 -> { - if (CimProfile18.isHeaderProfile(graph)) { - // If the graph contains header profile --> it is still CIM16 style - yield new CimProfile18(graph, true); - } - if (!CimProfile18.hasOntology(graph)) { - throw new IllegalArgumentException("Graph does not contain the required ontology subject for a CIM profile."); - } - if (!CimProfile18.hasVersionIRIAndKeyword(graph)) { - throw new IllegalArgumentException("Graphs ontology does not contain the required versionIRI and keyword for a CIM profile."); - } - yield new CimProfile18(graph, false); - } - case NO_CIM -> throw new IllegalArgumentException("Graph does not appear to be a CIM graph. No proper 'cim' namespace defined."); - }; + if (!this.getCimNamespace().equals(other.getCimNamespace())) { + return false; } + if (isHeaderProfile()) { + return other.isHeaderProfile(); + } + return this.getOwlVersionIris().equals(other.getOwlVersionIris()); + } + /** + * Calculates the hash code for this profile. If the model is a header profile, the hash code is + * based on the CIM version and the fact that it is a header profile. If the model is not a header + * profile, the hash code is based on the CIM version and the set of version IRIs. + * + * @return The hash code for this profile. + */ + default int calculateHashCode() { + // hash code from isHeader, cim namespace and version IRIs + int result = Boolean.hashCode(isHeaderProfile()); + result = 31 * result + getCimNamespace().hashCode(); + if (!isHeaderProfile()) { + result = 31 * result + getOwlVersionIris().hashCode(); + } + return result; + } } diff --git a/cimxml/src/main/java/de/soptim/opencgmes/cimxml/graph/CimProfile16.java b/cimxml/src/main/java/de/soptim/opencgmes/cimxml/graph/CimProfile16.java index 7aaf2a10..f23ea84e 100644 --- a/cimxml/src/main/java/de/soptim/opencgmes/cimxml/graph/CimProfile16.java +++ b/cimxml/src/main/java/de/soptim/opencgmes/cimxml/graph/CimProfile16.java @@ -18,139 +18,151 @@ package de.soptim.opencgmes.cimxml.graph; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; import org.apache.jena.graph.Graph; import org.apache.jena.graph.Node; import org.apache.jena.graph.NodeFactory; import org.apache.jena.sparql.graph.GraphWrapper; import org.apache.jena.vocabulary.RDF; -import java.util.Set; -import java.util.stream.Collectors; -import java.util.stream.Stream; - /** - * A wrapper for a graph that contains a CIM profile ontology as defined using CIM 16 + * A wrapper for a graph that contains a CIM profile ontology as defined using CIM 16. */ public class CimProfile16 extends GraphWrapper implements CimProfile { - final static String NS_RDFS = "http://www.w3.org/2000/01/rdf-schema#"; - - /** - * This is how the profile IRI ends in CGMES 2.4.15. - * Example: "http://entsoe.eu/CIM/SchemaExtension/3/1#EquipmentVersion" - */ - final static String PROFILE_VERSION_POSTFIX = "Version"; - - final static Node PREDICATE_RDFS_DOMAIN = NodeFactory.createURI(NS_RDFS + "domain"); - final static Node PREDICATE_CIMS_IS_FIXED = NodeFactory.createURI(NS_CIMS + "isFixed"); - - /** - * Checks if the given graph contains the required information to be wrapped as a CimProfile. - * It must contain exactly one fixed text for the property cim:shortName. - * It may contain zero or more fixed texts for the properties cim:entsoeURI or cim:baseURI. - * If the graph contains at least one fixed text for cim:entsoeURI or cim:baseURI. - * @param graph The graph to check. - * @return true if the graph can be wrapped as a CimProfile, false otherwise. - */ - public static boolean hasVersionIRIAndKeyword(Graph graph) { - if (getProfilePropertyFixedTexts(graph, ".shortName").findAny().isEmpty()) { - return false; //no keyword defined - } - if (getProfilePropertyFixedTexts(graph, ".entsoeURI").findAny().isPresent()) { - return true; //at least one version IRI defined - } - if (getProfilePropertyFixedTexts(graph, ".baseURI").findAny().isPresent()) { - return true; //at least one version IRI defined - } - return false; //no version IRI defined - } - - /** - * Looks for all triples with rdfs:domain pointing to a profile class (i.e. ending with "Version") - * and with a subject starting with the profile class IRI + the given property name (including the dot). - * Then looks for all triples with cim:isFixed for the found profile class - * and returns the literal values of those triples. - * @param graph the graph to search in - * @param propertyNameStartWithIncludingDot the property name to look for, including the dot (e.g. ".shortName", ".entsoeURI", ".baseURI") - * @return a stream of fixed text values for the given property name - */ - private static Stream getProfilePropertyFixedTexts(Graph graph, String propertyNameStartWithIncludingDot) { - return graph.stream(Node.ANY, PREDICATE_RDFS_DOMAIN, Node.ANY) //first look for the domain - .filter(t - -> t.getObject().isURI() - && t.getObject().getURI().endsWith(PROFILE_VERSION_POSTFIX) - && t.getSubject().isURI() - && t.getSubject().getURI().startsWith(t.getObject().getURI()) - && t.getSubject().getURI().regionMatches(t.getObject().getURI().length(), - propertyNameStartWithIncludingDot,0, propertyNameStartWithIncludingDot.length())) - .flatMap(t -> graph - .stream(t.getSubject(), PREDICATE_CIMS_IS_FIXED, Node.ANY) - .filter(t2 -> t2.getObject().isLiteral()) - .map(t2 -> t2.getObject().getLiteralLexicalForm())); - } - - private final boolean isHeaderProfile; - - /** - * Wraps the given graph as a CimProfile16. - * @param graph The graph to wrap. - * @throws IllegalArgumentException if the graph does not contain the required information to be a CimProfile16. - */ - public CimProfile16(Graph graph, boolean isHeaderProfile) { - super(graph); - this.isHeaderProfile = isHeaderProfile; + public static final String CIM_NAMESPACE = "http://iec.ch/TC57/2013/CIM-schema-cim16#"; + + static final String NS_RDFS = "http://www.w3.org/2000/01/rdf-schema#"; + + /** + * This is how the profile IRI ends in CGMES 2.4.15. Example: + * {@code http://entsoe.eu/CIM/SchemaExtension/3/1#EquipmentVersion} + */ + static final String PROFILE_VERSION_POSTFIX = "Version"; + + static final Node PREDICATE_RDFS_DOMAIN = NodeFactory.createURI(NS_RDFS + "domain"); + static final Node PREDICATE_CIMS_IS_FIXED = NodeFactory.createURI(NS_CIMS + "isFixed"); + + /** + * Wraps the given graph as a CimProfile16. + * + * @param graph The graph to wrap. + * @throws IllegalArgumentException if the graph does not contain the required information to be a + * CimProfile16. + */ + public CimProfile16(Graph graph) { + super(graph); + if (isCim16HeaderProfile(graph)) { + return; } - - @Override - public boolean isHeaderProfile() { - return this.isHeaderProfile; + if (!hasVersionIriAndKeyword(graph)) { + throw new IllegalArgumentException( + "Graph does not contain the required '...Version.shortName' and '...Version.entsoeURI*'" + + " or '...Version.baseURI...' for a CGMES 2.4.15 profile."); } - - @Override - public String getDcatKeyword() { - if (isHeaderProfile) { - // CGMES 2.4.15 file header profiles do not have a keyword. - return "DH"; // Use "DH" for compatibility with old CGMES 2.4.15 file header profiles. - } - return getProfilePropertyFixedTexts(get(), ".shortName") - .findFirst().orElse(null); + } + + /** + * Checks if the given graph contains the required information to be wrapped as a CimProfile. It + * must contain exactly one fixed text for the property cim:shortName. It may contain zero or more + * fixed texts for the properties cim:entsoeURI or cim:baseURI. If the graph contains at least one + * fixed text for cim:entsoeURI or cim:baseURI. + * + * @param graph The graph to check. + * @return true if the graph can be wrapped as a CimProfile, false otherwise. + */ + public static boolean hasVersionIriAndKeyword(Graph graph) { + if (getProfilePropertyFixedTexts(graph, ".shortName") + .findAny().isEmpty()) { + return false; // no keyword defined } - - @Override - public Set getOwlVersionIRIs() { - return Stream.concat( - getProfilePropertyFixedTexts(get(), ".entsoeURI"), - getProfilePropertyFixedTexts(get(), ".baseURI")) - .map(NodeFactory::createURI) - .collect(Collectors.toUnmodifiableSet()); + if (getProfilePropertyFixedTexts(graph, ".entsoeURI") + .findAny().isPresent()) { + return true; // at least one version IRI defined } - - @Override - public String getOwlVersionInfo() { - return null; + return getProfilePropertyFixedTexts(graph, ".baseURI") + .findAny().isPresent(); // at least one version IRI defined or no version IRI defined + } + + /** + * Looks for all triples with rdfs:domain pointing to a profile class (i.e. ending with "Version") + * and with a subject starting with the profile class IRI + the given property name (including the + * dot). Then looks for all triples with cim:isFixed for the found profile class and returns the + * literal values of those triples. + * + * @param graph the graph to search in + * @param propertyNameStartWithIncludingDot the property name to look for, including the dot (e.g. + * ".shortName", ".entsoeURI", ".baseURI") + * @return a stream of fixed text values for the given property name + */ + private static Stream getProfilePropertyFixedTexts(Graph graph, + String propertyNameStartWithIncludingDot) { + return graph.stream(Node.ANY, PREDICATE_RDFS_DOMAIN, Node.ANY) // first look for the domain + .filter(t + -> t.getObject().isURI() + && t.getObject().getURI().endsWith(PROFILE_VERSION_POSTFIX) + && t.getSubject().isURI() + && t.getSubject().getURI().startsWith(t.getObject().getURI()) + && t.getSubject().getURI().regionMatches(t.getObject().getURI().length(), + propertyNameStartWithIncludingDot, 0, propertyNameStartWithIncludingDot.length())) + .flatMap(t -> graph + .stream(t.getSubject(), PREDICATE_CIMS_IS_FIXED, Node.ANY) + .filter(t2 -> t2.getObject().isLiteral()) + .map(t2 -> t2.getObject().getLiteralLexicalForm())); + } + + @Override + public boolean isHeaderProfile() { + return isCim16HeaderProfile(this); + } + + @Override + public String getDcatKeyword() { + if (isHeaderProfile()) { + // CGMES 2.4.15 file header profiles do not have a keyword. + return "FH"; // Use "FH" for compatibility with new CGMES 3.0 file header profiles. } - - @Override - public final boolean equals(Object other) { - if (!(other instanceof CimProfile16 that)) return false; - - return this.equals(that); - } - - @Override - public int hashCode() { - return this.calculateHashCode(); - } - - /** - * Determines if the given graph is a header profile. - * In CIM 16, header profiles are identified by the presence of a - * cim:Category with the value "PackageFileHeaderProfile". - */ - public static boolean isHeaderProfile(Graph graph) { - return graph.stream(Node.ANY, RDF.type.asNode(), TYPE_CLASS_CATEGORY) - .anyMatch(t - -> t.getSubject().isURI() - && t.getSubject().getURI().endsWith(PACKAGE_FILE_HEADER_PROFILE)); + return getProfilePropertyFixedTexts(get(), ".shortName") + .findFirst().orElse(null); + } + + @Override + public Set getOwlVersionIris() { + return Stream.concat( + getProfilePropertyFixedTexts(get(), ".entsoeURI"), + getProfilePropertyFixedTexts(get(), ".baseURI")) + .map(NodeFactory::createURI) + .collect(Collectors.toUnmodifiableSet()); + } + + @Override + public String getOwlVersionInfo() { + return null; + } + + @Override + public final boolean equals(Object other) { + if (!(other instanceof CimProfile16 that)) { + return false; } + return this.equals(that); + } + + @Override + public int hashCode() { + return this.calculateHashCode(); + } + + /** + * Determines if the given graph is a header profile. In CIM 16, header profiles are identified by + * the presence of a cim:Category with the value "PackageFileHeaderProfile". + */ + public static boolean isCim16HeaderProfile(Graph graph) { + return graph.stream(Node.ANY, RDF.type.asNode(), TYPE_CLASS_CATEGORY) + .anyMatch(t + -> t.getSubject().isURI() + && t.getSubject().getURI().endsWith(PACKAGE_FILE_HEADER_PROFILE)); + } } diff --git a/cimxml/src/main/java/de/soptim/opencgmes/cimxml/graph/CimProfile17.java b/cimxml/src/main/java/de/soptim/opencgmes/cimxml/graph/CimProfile17.java index f13cd82a..4bfda886 100644 --- a/cimxml/src/main/java/de/soptim/opencgmes/cimxml/graph/CimProfile17.java +++ b/cimxml/src/main/java/de/soptim/opencgmes/cimxml/graph/CimProfile17.java @@ -18,6 +18,10 @@ package de.soptim.opencgmes.cimxml.graph; +import java.util.NoSuchElementException; +import java.util.Set; +import java.util.function.Predicate; +import java.util.stream.Collectors; import org.apache.jena.graph.Graph; import org.apache.jena.graph.Node; import org.apache.jena.graph.NodeFactory; @@ -25,110 +29,156 @@ import org.apache.jena.sparql.graph.GraphWrapper; import org.apache.jena.vocabulary.RDF; -import java.util.Set; -import java.util.stream.Collectors; - /** * A wrapper for a graph that contains a CIM profile ontology as defined in CIM 17. */ public class CimProfile17 extends GraphWrapper implements CimProfile { - final static String NS_OWL = "http://www.w3.org/2002/07/owl#"; - final static String NS_DCAT = "http://www.w3.org/ns/dcat#"; - final static Node CLASS_ONTOLOGY = NodeFactory.createURI(NS_OWL + "Ontology"); - final static Node PREDICATE_DCAT_KEYWORD = NodeFactory.createURI(NS_DCAT + "keyword"); - final static Node PREDICATE_OWL_VERSION_IRI = NodeFactory.createURI(NS_OWL + "versionIRI"); - final static Node PREDICATE_OWL_VERSION_INFO = NodeFactory.createURI(NS_OWL + "versionInfo"); - - /** - * Find the ontology node in the graph. - * The ontology node is defined as a subject with type owl:Ontology. - * If no such node is found, null is returned. - * - * @param graph The graph to search in. - * @return The ontology node or null if not found. - */ - static boolean hasOntology(Graph graph) { - return graph.find(Node.ANY, RDF.type.asNode(), CLASS_ONTOLOGY).hasNext(); - } - - public static Node getOntology(Graph graph) { - return graph.stream(Node.ANY, RDF.type.asNode(), CLASS_ONTOLOGY).findAny().map(Triple::getSubject).orElseThrow(); - } - - public Node getOntology() { - return getOntology(this); - } - - public static boolean hasVersionIRIAndKeyword(Graph graph) { - return graph.find(Node.ANY, PREDICATE_DCAT_KEYWORD, Node.ANY).hasNext() - && graph.find(Node.ANY, PREDICATE_OWL_VERSION_IRI, Node.ANY).hasNext(); - } - - private final boolean isHeaderProfile; - - /** - * Wraps the given graph as a CimProfile17. - * @param graph The graph to wrap. - * @throws IllegalArgumentException if the graph does not contain the required information to be a CimProfile17. - */ - public CimProfile17(Graph graph, boolean isHeaderProfile) { - super(graph); - this.isHeaderProfile = isHeaderProfile; - } - - @Override - public boolean isHeaderProfile() { - return this.isHeaderProfile; - } - - @Override - public String getDcatKeyword() { - if (isHeaderProfile) { - // CGMES v3.0 file header profiles do not have a keyword. - return "DH"; // Use "DH" for compatibility with old CGMES 2.4.15 file header profiles. - } - var iter = find(getOntology(), PREDICATE_DCAT_KEYWORD, Node.ANY); - return iter.hasNext() ? iter.next().getObject().getLiteralValue().toString() : null; + public static final String CIM_NAMESPACE = "http://iec.ch/TC57/CIM100#"; + + protected static final String NS_OWL = "http://www.w3.org/2002/07/owl#"; + protected static final Node CLASS_ONTOLOGY = NodeFactory.createURI(NS_OWL + "Ontology"); + protected static final String NS_DCAT = "http://www.w3.org/ns/dcat#"; + protected static final Node PREDICATE_DCAT_KEYWORD = NodeFactory.createURI(NS_DCAT + "keyword"); + protected static final Node PREDICATE_OWL_VERSION_IRI = NodeFactory.createURI( + NS_OWL + "versionIRI"); + protected static final Node PREDICATE_OWL_VERSION_INFO = NodeFactory.createURI( + NS_OWL + "versionInfo"); + + /** + * Wraps the given graph as a CimProfile17. + * + * @param graph The graph to wrap. + * @throws IllegalArgumentException if the graph does not contain the required information to be a + * CimProfile17. + */ + public CimProfile17(Graph graph) { + this(graph, CimProfile17::isCim17HeaderProfile); + } + + /** + * Wraps the given graph as a CimProfile17, using the provided predicate to determine if the graph + * is a header profile. + * + * @param graph The graph to wrap. + * @param isHeaderProfilePredicate A predicate that determines if the graph is a header profile. + * @throws IllegalArgumentException if the graph does not contain the required information to be a + * CimProfile17. + */ + protected CimProfile17(Graph graph, Predicate isHeaderProfilePredicate) { + super(graph); + if (isHeaderProfilePredicate.test(graph)) { + return; } - - @Override - public Set getOwlVersionIRIs() { - return stream(getOntology(), PREDICATE_OWL_VERSION_IRI, Node.ANY) - .map(Triple::getObject) - .collect(Collectors.toUnmodifiableSet()); - } - - @Override - public String getOwlVersionInfo() { - var iter = find(getOntology(), PREDICATE_OWL_VERSION_INFO, Node.ANY); - return iter.hasNext() ? iter.next().getObject().getLiteralValue().toString() : null; + if (!hasOntology(graph)) { + throw new IllegalArgumentException( + "Graph does not contain the required ontology subject for a CIM profile."); } - - @Override - public final boolean equals(Object other) { - if (!(other instanceof CimProfile17 that)) return false; - - return this.equals(that); + if (!hasVersionIriAndKeyword(graph)) { + throw new IllegalArgumentException("Graphs ontology does not contain the required versionIRI" + + " and keyword for a CIM profile."); } + } - @Override - public int hashCode() { - return this.calculateHashCode(); + @Override + public String getDcatKeyword() { + if (isHeaderProfile()) { + // CGMES v3.0 file header profiles do not have a keyword. + return "FH"; // Use "FH" for compatibility with old CGMES 2.4.15 file header profiles. } - - /** - * Checks if the given graph is a header profile. - * A header profile is defined as a graph that contains a subject with type - * "cims:ClassCategory" and - * the subject URI ends with "#Package_FileHeaderProfile". - * @param graph The graph to check. - * @return true if the graph is a header profile, false otherwise. - */ - public static boolean isHeaderProfile(Graph graph) { - return graph.stream(Node.ANY, RDF.type.asNode(), TYPE_CLASS_CATEGORY) - .anyMatch(t - -> t.getSubject().isURI() - && t.getSubject().getURI().endsWith(PACKAGE_FILE_HEADER_PROFILE)); + var iter = find(getOntology(), PREDICATE_DCAT_KEYWORD, Node.ANY); + return iter.hasNext() ? iter.next().getObject().getLiteralValue().toString() : null; + } + + @Override + public Set getOwlVersionIris() { + return stream(getOntology(), PREDICATE_OWL_VERSION_IRI, Node.ANY) + .map(Triple::getObject) + .collect(Collectors.toUnmodifiableSet()); + } + + @Override + public String getOwlVersionInfo() { + var iter = find(getOntology(), PREDICATE_OWL_VERSION_INFO, Node.ANY); + return iter.hasNext() ? iter.next().getObject().getLiteralValue().toString() : null; + } + + @Override + public final boolean equals(Object other) { + if (!(other instanceof CimProfile17 that)) { + return false; } + return this.equals(that); + } + + @Override + public int hashCode() { + return this.calculateHashCode(); + } + + /** + * Get the ontology node in this graph. The ontology node is defined as a subject with type + * owl:Ontology. If no such node is found, an exception is thrown. + * + * @return The ontology node. + * @throws NoSuchElementException if no ontology node is found. + */ + public Node getOntology() { + return getOntology(this); + } + + /** + * Get the ontology node in the graph. The ontology node is defined as a subject with type + * owl:Ontology. If no such node is found, an exception is thrown. + * + * @param graph The graph to search in. + * @return The ontology node. + * @throws NoSuchElementException if no ontology node is found. + */ + public static Node getOntology(Graph graph) { + return graph.stream(Node.ANY, RDF.type.asNode(), CLASS_ONTOLOGY).findAny() + .map(Triple::getSubject).orElseThrow(); + } + + /** + * Find the ontology node in the graph. The ontology node is defined as a subject with type + * owl:Ontology. If no such node is found, null is returned. + * + * @param graph The graph to search in. + * @return The ontology node or null if not found. + */ + static boolean hasOntology(Graph graph) { + return graph.find(Node.ANY, RDF.type.asNode(), CLASS_ONTOLOGY).hasNext(); + } + + /** + * Checks if the given graph contains both a DCAT keyword and an OWL version IRI. + * + * @param graph The graph to check. + * @return true if both a DCAT keyword and an OWL version IRI are present, false otherwise. + */ + public static boolean hasVersionIriAndKeyword(Graph graph) { + return graph.find(Node.ANY, PREDICATE_DCAT_KEYWORD, Node.ANY).hasNext() + && graph.find(Node.ANY, PREDICATE_OWL_VERSION_IRI, Node.ANY).hasNext(); + } + + @Override + public boolean isHeaderProfile() { + return isCim17HeaderProfile(this); + } + + /** + * Checks if the given graph is a header profile. A header profile is defined as a graph that + * contains a subject with type "cims:ClassCategory" and the subject URI ends with + * "#Package_FileHeaderProfile". + * + * @param graph The graph to check. + * @return true if the graph is a header profile, false otherwise. + */ + public static boolean isCim17HeaderProfile(Graph graph) { + return graph.stream(Node.ANY, RDF.type.asNode(), TYPE_CLASS_CATEGORY) + .anyMatch(t + -> t.getSubject().isURI() + && t.getSubject().getURI().endsWith(PACKAGE_FILE_HEADER_PROFILE)); + } } diff --git a/cimxml/src/main/java/de/soptim/opencgmes/cimxml/graph/CimProfile18.java b/cimxml/src/main/java/de/soptim/opencgmes/cimxml/graph/CimProfile18.java index a15b477a..2d4c16d8 100644 --- a/cimxml/src/main/java/de/soptim/opencgmes/cimxml/graph/CimProfile18.java +++ b/cimxml/src/main/java/de/soptim/opencgmes/cimxml/graph/CimProfile18.java @@ -22,48 +22,47 @@ import org.apache.jena.graph.Node; /** - * A wrapper for a graph that contains a CIM profile ontology as defined in CIM 18 + * A wrapper for a graph that contains a CIM profile ontology as defined in CIM 18. */ -public class CimProfile18 extends CimProfile17 implements CimProfile { +public class CimProfile18 extends CimProfile17 { - final static String DOCUMENT_HEADER_VERSION_IRI_START = "https://ap-voc.cim4.eu/DocumentHeader"; + public static final String CIM_NAMESPACE = "https://cim.ucaiug.io/ns#"; + private static final String DOCUMENT_HEADER_VERSION_IRI_START = "https://ap-voc.cim4.eu/DocumentHeader"; - /** - * Wraps the given graph as a CimProfile18. - * @param graph The graph to wrap. - * @throws IllegalArgumentException if the graph does not contain the required information to be a CimProfile18. - */ - public CimProfile18(Graph graph, boolean isHeaderProfile) { - super(graph, isHeaderProfile); - } - - /** - * Checks if the given graph contains both a DCAT keyword and an OWL version IRI. - * @param graph The graph to check. - * @return true if both a DCAT keyword and an OWL version IRI are present, false otherwise. - */ - public static boolean hasVersionIRIAndKeyword(Graph graph) { - return graph.find(Node.ANY, PREDICATE_DCAT_KEYWORD, Node.ANY).hasNext() - && graph.find(Node.ANY, PREDICATE_OWL_VERSION_IRI, Node.ANY).hasNext(); - } + /** + * Wraps the given graph as a CimProfile18. + * + * @param graph The graph to wrap. + * @throws IllegalArgumentException if the graph does not contain the required information to be a + * CimProfile18. + */ + public CimProfile18(Graph graph) { + super(graph, CimProfile18::isCim18HeaderProfile); + } - /** - * Checks if the given graph is a header profile. - * A header profile is identified by having an ontology with a version IRI - * that starts with "https://ap-voc.cim4.eu/DocumentHeader". - * - * @param graph The graph to check. - * @return true if the graph is a header profile, false otherwise. - */ - public static boolean isHeaderProfile(Graph graph) { - if (!hasOntology(graph)) - return false; - var ontology = getOntology(graph); + @Override + public boolean isHeaderProfile() { + return isCim18HeaderProfile(this); + } - // look for https://ap.cim4.eu/DocumentHeader# without # in version IRIs - return graph.stream(ontology, PREDICATE_OWL_VERSION_IRI, Node.ANY) - .anyMatch(t - -> t.getObject().isURI() - && t.getObject().getURI().startsWith(DOCUMENT_HEADER_VERSION_IRI_START)); + /** + * Checks if the given graph is a header profile. A header profile is identified by having an + * ontology with a version IRI that starts with + * {@code https://ap-voc.cim4.eu/DocumentHeader}. + * + * @param graph The graph to check. + * @return true if the graph is a header profile, false otherwise. + */ + public static boolean isCim18HeaderProfile(Graph graph) { + if (!hasOntology(graph)) { + return false; } + var ontology = getOntology(graph); + + // look for https://ap.cim4.eu/DocumentHeader# without # in version IRIs + return graph.stream(ontology, PREDICATE_OWL_VERSION_IRI, Node.ANY) + .anyMatch(t + -> t.getObject().isURI() + && t.getObject().getURI().startsWith(DOCUMENT_HEADER_VERSION_IRI_START)); + } } diff --git a/cimxml/src/main/java/de/soptim/opencgmes/cimxml/graph/DisjointMultiUnion.java b/cimxml/src/main/java/de/soptim/opencgmes/cimxml/graph/DisjointMultiUnion.java index edb21568..393b56c5 100644 --- a/cimxml/src/main/java/de/soptim/opencgmes/cimxml/graph/DisjointMultiUnion.java +++ b/cimxml/src/main/java/de/soptim/opencgmes/cimxml/graph/DisjointMultiUnion.java @@ -18,6 +18,8 @@ package de.soptim.opencgmes.cimxml.graph; +import java.util.Iterator; +import java.util.stream.Stream; import org.apache.jena.graph.Graph; import org.apache.jena.graph.Node; import org.apache.jena.graph.Triple; @@ -25,74 +27,122 @@ import org.apache.jena.util.iterator.ExtendedIterator; import org.apache.jena.util.iterator.NullIterator; -import java.util.Iterator; -import java.util.stream.Stream; - /** - * Extends the Apache Jena MultiUnion but find does not elminiate duplicates - * This is based on from https://github.com/apache/jena/blob/master/jena-core/src/main/java/org/apache/jena/graph/compose/MultiUnion.java + * Extends the Apache Jena MultiUnion but find does not elminiate duplicates This is based on from + * . */ public class DisjointMultiUnion extends MultiUnion { - public DisjointMultiUnion() { - super(); - } + /** + * Create an empty MultiUnion. Graphs can be added to the union using addGraph. + */ + public DisjointMultiUnion() { + super(); + } - public DisjointMultiUnion(Graph... graphs) { - super(graphs); - } + /** + * Create a MultiUnion with the given graphs as components. + * + * @param graphs the component graphs of the union + */ + public DisjointMultiUnion(Graph... graphs) { + super(graphs); + } - public DisjointMultiUnion(Iterator graphs) { - super(graphs); - } + /** + * Create a MultiUnion with the given graphs as components. + * + * @param graphs the component graphs of the union + */ + public DisjointMultiUnion(Iterator graphs) { + super(graphs); + } - /** - *

- * Answer an iterator over the triples in the union of the graphs in this composition. Note - * that the requirement to remove duplicates from the union means that this will be an - * expensive operation for large (and especially for persistent) graphs. - *

- * - * @param t The matcher to match against - * - * @return An iterator of all triples matching t in the union of the graphs. - */ - @Override - public ExtendedIterator graphBaseFind(Triple t) { // optimise the case where there's only one component graph. - ExtendedIterator iter = NullIterator.instance(); - for(var g: m_subGraphs) { - iter = iter.andThen(g.find(t)); - } - return iter; + /** + * Answer an iterator over the triples in the union of the graphs in this composition. + * + *

Note + * that this implementation does not eliminate duplicates, so if the same triple appears in + * multiple component graphs, it will appear multiple times in the result. + * This is a deviation from the standard MultiUnion implementation, which eliminates duplicates. + * + * @param t The matcher to match against + * @return An iterator of all triples matching t in the union of the graphs. + */ + @Override + public ExtendedIterator graphBaseFind( + Triple t) { // optimise the case where there's only one component graph. + ExtendedIterator iter = NullIterator.instance(); + for (var g : m_subGraphs) { + iter = iter.andThen(g.find(t)); } + return iter; + } - @Override - public ExtendedIterator find() { - ExtendedIterator iter = NullIterator.instance(); - for(var g: m_subGraphs) { - iter = iter.andThen(g.find()); - } - return iter; + /** + * Answer an iterator over all the triples in the union of the graphs in this composition. + * This implementation does not eliminate duplicates, so if the same triple appears in multiple + * component graphs, it will appear multiple times in the result. + * This is a deviation from the standard MultiUnion implementation, which eliminates duplicates. + * + * @return An iterator of all triples in the union of the graphs. + */ + @Override + public ExtendedIterator find() { + ExtendedIterator iter = NullIterator.instance(); + for (var g : m_subGraphs) { + iter = iter.andThen(g.find()); } + return iter; + } - @Override - public Stream stream(Node s, Node p, Node o) { - return m_subGraphs.stream() - .flatMap(g -> g.stream(s, p, o)); - } + /** + * Answer a stream over the triples in the union of the graphs in this composition. + * This implementation does not eliminate duplicates, so if the same triple appears in multiple + * component graphs, it will appear multiple times in the result. + * This is a deviation from the standard MultiUnion implementation, which eliminates duplicates. + * + * @param s the subject node to match, or Node.ANY for a wildcard + * @param p the predicate node to match, or Node.ANY for a wildcard + * @param o the object node to match, or Node.ANY for a wildcard + * @return a stream of all triples matching the given pattern in the union of the graphs. + */ + @Override + public Stream stream(Node s, Node p, Node o) { + return m_subGraphs.stream() + .flatMap(g -> g.stream(s, p, o)); + } - @Override - public Stream stream() { - return m_subGraphs.stream() - .flatMap(Graph::stream); - } + /** + * Answer a stream over all the triples in the union of the graphs in this composition. + * This implementation does not eliminate duplicates, so if the same triple appears in multiple + * component graphs, it will appear multiple times in the result. + * This is a deviation from the standard MultiUnion implementation, which eliminates duplicates. + * + * @return a stream of all triples in the union of the graphs. + */ + @Override + public Stream stream() { + return m_subGraphs.stream() + .flatMap(Graph::stream); + } - @Override - protected int graphBaseSize() { - var size = 0; - for(var g: m_subGraphs) { - size += g.size(); - } - return size; + /** + * Answer the number of triples in the union of the graphs in this composition. + * This implementation does not eliminate duplicates, so if the same triple appears in multiple + * component graphs, it will be counted multiple times in the result. + * This is a deviation from the standard MultiUnion implementation, which eliminates duplicates. + * + * @return The number of triples in the union of the graphs. + */ + @Override + protected int graphBaseSize() { + var size = 0; + for (var g : m_subGraphs) { + size += g.size(); } + return size; + } } diff --git a/cimxml/src/main/java/de/soptim/opencgmes/cimxml/graph/FastDeltaGraph.java b/cimxml/src/main/java/de/soptim/opencgmes/cimxml/graph/FastDeltaGraph.java index 376e2ce1..d3285b89 100644 --- a/cimxml/src/main/java/de/soptim/opencgmes/cimxml/graph/FastDeltaGraph.java +++ b/cimxml/src/main/java/de/soptim/opencgmes/cimxml/graph/FastDeltaGraph.java @@ -18,142 +18,185 @@ package de.soptim.opencgmes.cimxml.graph; +import java.util.Iterator; +import java.util.stream.Stream; import org.apache.jena.graph.Graph; import org.apache.jena.graph.Node; import org.apache.jena.graph.Triple; import org.apache.jena.graph.compose.Delta; import org.apache.jena.graph.impl.GraphBase; -import org.apache.jena.mem2.GraphMem2Roaring; -import org.apache.jena.mem2.IndexingStrategy; +import org.apache.jena.sparql.graph.GraphFactory; import org.apache.jena.util.iterator.ExtendedIterator; -import java.util.Iterator; -import java.util.stream.Stream; - /** - * Faster alternative to {@link Delta} + * Faster alternative to {@link Delta}. */ public class FastDeltaGraph extends GraphBase { - private final Graph base; - private final Graph additions; - private final Graph deletions; - - public FastDeltaGraph(Graph base) { - super(); - if (base == null) - throw new IllegalArgumentException("base graph must not be null"); - this.base = base; - this.additions = new GraphMem2Roaring(IndexingStrategy.LAZY_PARALLEL); - this.deletions = new GraphMem2Roaring(IndexingStrategy.LAZY_PARALLEL); - } - - /** - * Creates a new {@link FastDeltaGraph} that is based on the given {@code newBase} graph. - * This is used to rebase a {@link FastDeltaGraph} on a new base graph. - * There are no checks performed to ensure that the new base graph is compatible with the - * previous base graph. - * @param newBase the new base graph - * @param deltaGraphToRebase the delta graph to rebase - */ - public FastDeltaGraph(Graph newBase, FastDeltaGraph deltaGraphToRebase) { - super(); - if (newBase == null) - throw new IllegalArgumentException("base graph must not be null"); - this.base = newBase; - this.additions = deltaGraphToRebase.additions; - this.deletions = deltaGraphToRebase.deletions; - } - - public FastDeltaGraph(Graph base, Graph additions, Graph deletions) { - super(); - if (base == null) - throw new IllegalArgumentException("base graph must not be null"); - this.base = base; - this.additions = additions; - this.deletions = deletions; - } - - public Iterator getAdditions() { - return additions.find(); - } - - public Iterator getDeletions() { - return deletions.find(); - } - - public boolean hasChanges() { - return !additions.isEmpty() || !deletions.isEmpty(); - } - - public Graph getBase() { - return base; - } - - @Override - public void performAdd(Triple t) { - if (!base.contains(t)) - additions.add(t); - deletions.delete(t); - } - - @Override - public void performDelete(Triple t) { - additions.delete(t); - if (base.contains(t)) - deletions.add(t); - } - - @Override - protected boolean graphBaseContains(Triple t) { - if (t.isConcrete()) { - if (base.contains(t)) { - return !deletions.contains(t); - } - return additions.contains(t); - } else { - return graphBaseFind(t).hasNext(); - } - } - - @Override - protected ExtendedIterator graphBaseFind(Triple triplePattern) { - return base.find(triplePattern) - .filterDrop(deletions::contains) - .andThen(additions.find(triplePattern)); - } - - @Override - public ExtendedIterator find() { - return base.find() - .filterDrop(deletions::contains) - .andThen(additions.find()); - } - - @Override - public Stream stream() { - return Stream.concat( - base.stream().filter(t -> !deletions.contains(t)), - additions.stream()); - } - - @Override - public Stream stream(Node s, Node p, Node o) { - return Stream.concat( - base.stream(s, p, o).filter(t -> !deletions.contains(t)), - additions.stream(s, p, o)); - } - - @Override - public void close() { - super.close(); - base.close(); - additions.close(); - deletions.close(); - } - - @Override - public int graphBaseSize() { - return base.size() + additions.size() - deletions.size(); - } + private final Graph base; + private final Graph additions; + private final Graph deletions; + + /** + * Creates a new {@link FastDeltaGraph} that is based on the given {@code base} graph. The delta + * graph will initially be empty, i.e. it will not contain any additions or deletions. + * + * @param base the base graph on which this delta graph is based + */ + public FastDeltaGraph(Graph base) { + super(); + if (base == null) { + throw new IllegalArgumentException("base graph must not be null"); + } + this.base = base; + this.additions = GraphFactory.createGraphMem(); + this.deletions = GraphFactory.createGraphMem(); + } + + /** + * Creates a new {@link FastDeltaGraph} that is based on the given {@code newBase} graph. This is + * used to rebase a {@link FastDeltaGraph} on a new base graph. There are no checks performed to + * ensure that the new base graph is compatible with the previous base graph. + * + * @param newBase the new base graph + * @param deltaGraphToRebase the delta graph to rebase + */ + public FastDeltaGraph(Graph newBase, FastDeltaGraph deltaGraphToRebase) { + super(); + if (newBase == null) { + throw new IllegalArgumentException("base graph must not be null"); + } + this.base = newBase; + this.additions = deltaGraphToRebase.additions; + this.deletions = deltaGraphToRebase.deletions; + } + + /** + * Creates a new {@link FastDeltaGraph} that is based on the given {@code base} graph and contains + * the given additions and deletions. This constructor is used to create a delta graph with + * predefined additions and deletions, for example when rebasing a delta graph on a new base + * graph. + * + * @param base the base graph on which this delta graph is based + * @param additions the graph containing the triples that are added in this delta graph + * @param deletions the graph containing the triples that are deleted in this delta graph + */ + public FastDeltaGraph(Graph base, Graph additions, Graph deletions) { + super(); + if (base == null) { + throw new IllegalArgumentException("base graph must not be null"); + } + this.base = base; + this.additions = additions; + this.deletions = deletions; + } + + /** + * Returns an iterator over the triples that are added in this delta graph. + * These are the triples that are present in the delta graph but not in the base graph. + * + * @return an iterator over the added triples in this delta graph + */ + public Iterator getAdditions() { + return additions.find(); + } + + /** + * Returns an iterator over the triples that are deleted in this delta graph. + * These are the triples that are present in the base graph but not in the delta graph. + * + * @return an iterator over the deleted triples in this delta graph + */ + public Iterator getDeletions() { + return deletions.find(); + } + + /** + * Checks if this delta graph has any changes compared to the base graph. A delta graph is + * considered to have changes if it contains any additions or deletions. + * + * @return true if this delta graph has any changes, false otherwise + */ + public boolean hasChanges() { + return !additions.isEmpty() || !deletions.isEmpty(); + } + + /** + * Returns the base graph on which this delta graph is based. The base graph represents the state + * of the graph before any additions or deletions are applied. + * + * @return the base graph of this delta graph + */ + public Graph getBase() { + return base; + } + + @Override + public void performAdd(Triple t) { + if (!base.contains(t)) { + additions.add(t); + } + deletions.delete(t); + } + + @Override + public void performDelete(Triple t) { + additions.delete(t); + if (base.contains(t)) { + deletions.add(t); + } + } + + @Override + protected boolean graphBaseContains(Triple t) { + if (t.isConcrete()) { + if (base.contains(t)) { + return !deletions.contains(t); + } + return additions.contains(t); + } else { + return graphBaseFind(t).hasNext(); + } + } + + @Override + protected ExtendedIterator graphBaseFind(Triple triplePattern) { + return base.find(triplePattern) + .filterDrop(deletions::contains) + .andThen(additions.find(triplePattern)); + } + + @Override + public ExtendedIterator find() { + return base.find() + .filterDrop(deletions::contains) + .andThen(additions.find()); + } + + @Override + public Stream stream() { + return Stream.concat( + base.stream().filter(t -> !deletions.contains(t)), + additions.stream()); + } + + @Override + public Stream stream(Node s, Node p, Node o) { + return Stream.concat( + base.stream(s, p, o).filter(t -> !deletions.contains(t)), + additions.stream(s, p, o)); + } + + @Override + public void close() { + super.close(); + base.close(); + additions.close(); + deletions.close(); + } + + @Override + public int graphBaseSize() { + return base.size() + additions.size() - deletions.size(); + } } diff --git a/cimxml/src/main/java/de/soptim/opencgmes/cimxml/parser/CimXmlParser.java b/cimxml/src/main/java/de/soptim/opencgmes/cimxml/parser/CimXmlParser.java index 5acf256d..4a3d56f4 100644 --- a/cimxml/src/main/java/de/soptim/opencgmes/cimxml/parser/CimXmlParser.java +++ b/cimxml/src/main/java/de/soptim/opencgmes/cimxml/parser/CimXmlParser.java @@ -19,27 +19,26 @@ package de.soptim.opencgmes.cimxml.parser; import de.soptim.opencgmes.cimxml.graph.CimProfile; -import de.soptim.opencgmes.cimxml.parser.system.StreamCIMXMLToDatasetGraph; +import de.soptim.opencgmes.cimxml.parser.system.StreamCimXmlToDatasetGraph; import de.soptim.opencgmes.cimxml.rdfs.CimProfileRegistry; import de.soptim.opencgmes.cimxml.rdfs.CimProfileRegistryStd; import de.soptim.opencgmes.cimxml.sparql.core.CimDatasetGraph; -import org.apache.commons.io.input.BufferedFileChannelInputStream; -import org.apache.jena.riot.system.ErrorHandler; -import org.apache.jena.riot.system.ErrorHandlerFactory; - import java.io.IOException; import java.io.InputStream; import java.io.Reader; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardOpenOption; +import org.apache.commons.io.input.BufferedFileChannelInputStream; +import org.apache.jena.riot.system.ErrorHandler; +import org.apache.jena.riot.system.ErrorHandlerFactory; /** * IEC 61970-552 CIMXML parser for OpenCGMES. * *

This parser provides specialized handling for Common Information Model (CIM) XML files - * as defined by the IEC 61970-552 standard. It extends standard RDF/XML parsing with - * CIM-specific features including:

+ * as defined by the IEC 61970-552 standard. It extends standard RDF/XML parsing with CIM-specific + * features including:

* *
    *
  • Automatic CIM version detection from namespace declarations
  • @@ -68,6 +67,7 @@ * } * *

    Thread Safety:

    + * *

    This class is thread-safe for parsing operations. The internal profile registry * is synchronized and can be safely accessed from multiple threads.

    * @@ -77,93 +77,102 @@ */ public class CimXmlParser { - private final ReaderCIMXML_StAX_SR reader; - private final CimProfileRegistry cimProfileRegistry; - private final RdfXmlParser rdfXmlParser; + private final ReaderCIMXML_StAX_SR reader; + private final CimProfileRegistry cimProfileRegistry; + private final RdfXmlParser rdfXmlParser; - /** - * Gets the error handler used by this parser. - * @return the error handler - */ - public ErrorHandler getErrorHandler() { - return reader.errorHandler; - } + /** + * Creates a new CIMXML parser with the standard error handler. + */ + public CimXmlParser() { + this(ErrorHandlerFactory.errorHandlerStd); + } - /** - * Gets the CIM profile registry used by this parser. - * @return the CIM profile registry - */ - public CimProfileRegistry getCimProfileRegistry() { - return cimProfileRegistry; - } + /** + * Creates a new CIMXML parser with the given error handler. + * + * @param errorHandler the error handler + */ + public CimXmlParser(final ErrorHandler errorHandler) { + this.reader = new ReaderCIMXML_StAX_SR(errorHandler); + this.rdfXmlParser = new RdfXmlParser(this.reader); + this.cimProfileRegistry = new CimProfileRegistryStd(); + } - /** - * Creates a new CIMXML parser with the standard error handler. - */ - public CimXmlParser() { - this(ErrorHandlerFactory.errorHandlerStd); - } + /** + * Gets the error handler used by this parser. + * + * @return the error handler + */ + public ErrorHandler getErrorHandler() { + return reader.errorHandler; + } - /** - * Creates a new CIMXML parser with the given error handler. - * @param errorHandler the error handler - */ - public CimXmlParser(final ErrorHandler errorHandler) { - this.reader = new ReaderCIMXML_StAX_SR(errorHandler); - this.rdfXmlParser = new RdfXmlParser(this.reader); - this.cimProfileRegistry = new CimProfileRegistryStd(); - } + /** + * Gets the CIM profile registry used by this parser. + * + * @return the CIM profile registry + */ + public CimProfileRegistry getCimProfileRegistry() { + return cimProfileRegistry; + } - /** - * Parses the CIM profile from the given path and registers it in the internal CIM profile registry. - * @param pathToCimProfile the path to the CIM profile - * @return the parsed CIM profile - * @throws IOException if an I/O error occurs - */ - public CimProfile parseAndRegisterCimProfile(final Path pathToCimProfile) throws IOException { - final var profile = rdfXmlParser.parseCimProfile(pathToCimProfile); - cimProfileRegistry.register(profile); - return profile; - } + /** + * Parses the CIM profile from the given path and registers it in the internal CIM profile + * registry. + * + * @param pathToCimProfile the path to the CIM profile + * @return the parsed CIM profile + * @throws IOException if an I/O error occurs + */ + public CimProfile parseAndRegisterCimProfile(final Path pathToCimProfile) throws IOException { + final var profile = rdfXmlParser.parseCimProfile(pathToCimProfile); + cimProfileRegistry.register(profile); + return profile; + } - /** - * Parses the CIMXML from the given reader and returns the resulting CIM dataset graph. - * @param reader the reader containing the CIMXML - * @return the resulting CIM dataset graph - */ - public CimDatasetGraph parseCimModel(final Reader reader) { - final var streamRDFProfile = new StreamCIMXMLToDatasetGraph(); - this.reader.read(reader, cimProfileRegistry, streamRDFProfile); - return streamRDFProfile.getCIMDatasetGraph(); - } + /** + * Parses the CIMXML from the given reader and returns the resulting CIM dataset graph. + * + * @param reader the reader containing the CIMXML + * @return the resulting CIM dataset graph + */ + public CimDatasetGraph parseCimModel(final Reader reader) { + final var streamRdfProfile = new StreamCimXmlToDatasetGraph(); + this.reader.read(reader, cimProfileRegistry, streamRdfProfile); + return streamRdfProfile.getCimDatasetGraph(); + } - /** - * Parses the CIMXML from the given input stream and returns the resulting CIM dataset graph. - * @param inputStream the input stream containing the CIMXML - * @return the resulting CIM dataset graph - */ - public CimDatasetGraph parseCimModel(final InputStream inputStream) { - final var streamRDFProfile = new StreamCIMXMLToDatasetGraph(); - this.reader.read(inputStream, cimProfileRegistry, streamRDFProfile); - return streamRDFProfile.getCIMDatasetGraph(); - } + /** + * Parses the CIMXML from the given input stream and returns the resulting CIM dataset graph. + * + * @param inputStream the input stream containing the CIMXML + * @return the resulting CIM dataset graph + */ + public CimDatasetGraph parseCimModel(final InputStream inputStream) { + final var streamRdfProfile = new StreamCimXmlToDatasetGraph(); + this.reader.read(inputStream, cimProfileRegistry, streamRdfProfile); + return streamRdfProfile.getCimDatasetGraph(); + } - /** - * Parses the CIMXML file at the given path and returns the resulting CIM dataset graph. - * @param pathToCimModel the path to the CIMXML file - * @return the resulting CIM dataset graph - * @throws IOException if an I/O error occurs - */ - public CimDatasetGraph parseCimModel(final Path pathToCimModel) throws IOException { - final var fileSize = Files.size(pathToCimModel); - final var streamRDFProfile = new StreamCIMXMLToDatasetGraph(); - try(final var is = new BufferedFileChannelInputStream.Builder() - .setPath(pathToCimModel) - .setOpenOptions(StandardOpenOption.READ) - .setBufferSize((fileSize > RdfXmlParser.MAX_BUFFER_SIZE) ? RdfXmlParser.MAX_BUFFER_SIZE : (int) fileSize) - .get()) { - this.reader.read(is, cimProfileRegistry, streamRDFProfile); - } - return streamRDFProfile.getCIMDatasetGraph(); + /** + * Parses the CIMXML file at the given path and returns the resulting CIM dataset graph. + * + * @param pathToCimModel the path to the CIMXML file + * @return the resulting CIM dataset graph + * @throws IOException if an I/O error occurs + */ + public CimDatasetGraph parseCimModel(final Path pathToCimModel) throws IOException { + final var fileSize = Files.size(pathToCimModel); + final var streamRdfProfile = new StreamCimXmlToDatasetGraph(); + try (var is = new BufferedFileChannelInputStream.Builder() + .setPath(pathToCimModel) + .setOpenOptions(StandardOpenOption.READ) + .setBufferSize((fileSize > RdfXmlParser.MAX_BUFFER_SIZE) ? RdfXmlParser.MAX_BUFFER_SIZE + : (int) fileSize) + .get()) { + this.reader.read(is, cimProfileRegistry, streamRdfProfile); } + return streamRdfProfile.getCimDatasetGraph(); + } } diff --git a/cimxml/src/main/java/de/soptim/opencgmes/cimxml/parser/ParserCIMXML_StAX_SR.java b/cimxml/src/main/java/de/soptim/opencgmes/cimxml/parser/ParserCIMXML_StAX_SR.java index b570b401..329666d4 100644 --- a/cimxml/src/main/java/de/soptim/opencgmes/cimxml/parser/ParserCIMXML_StAX_SR.java +++ b/cimxml/src/main/java/de/soptim/opencgmes/cimxml/parser/ParserCIMXML_StAX_SR.java @@ -19,9 +19,9 @@ package de.soptim.opencgmes.cimxml.parser; import de.soptim.opencgmes.cimxml.CimHeaderVocabulary; -import de.soptim.opencgmes.cimxml.CimVersion; import de.soptim.opencgmes.cimxml.CimXmlDocumentContext; -import de.soptim.opencgmes.cimxml.parser.system.StreamCIMXML; +import de.soptim.opencgmes.cimxml.graph.CimNamespaceFactoryRegistry; +import de.soptim.opencgmes.cimxml.parser.system.StreamCimXml; import de.soptim.opencgmes.cimxml.rdfs.CimProfileRegistry; import org.apache.commons.lang3.StringUtils; import org.apache.jena.atlas.io.IndentedWriter; @@ -68,6 +68,7 @@ * This implementation is based on the RDF/XML parser ParserRRX_StAX_SR in Apache Jena, originally. * It has been adapted to handle CIMXML specifics. */ +@SuppressWarnings("PMD") public class ParserCIMXML_StAX_SR { static { @@ -78,7 +79,7 @@ public class ParserCIMXML_StAX_SR { } } - private final static int IRI_CACHE_SIZE = 8192; + private static final int IRI_CACHE_SIZE = 8192; private final IndentedWriter trace; private final XMLStreamReader2 xmlSource; @@ -99,12 +100,12 @@ public class ParserCIMXML_StAX_SR { private boolean hasRDF = false; private boolean hasCimXmlNamespace = false; private boolean isCimXmlModel = false; - private CimVersion versionOfCIMXML = CimVersion.NO_CIM; + private String versionOfCIMXML = null; private final ErrorHandler errorHandler; - private final StreamCIMXML destination; + private final StreamCimXml destination; private final CimProfileRegistry cimProfileRegistry; - private final static String PROCESSING_INSTRUCTION_IEC61970_552 = "iec61970-552"; + private static final String PROCESSING_INSTRUCTION_IEC61970_552 = "iec61970-552"; private void updateCurrentIriCacheForCurrentBase() { if (currentBase != null) { @@ -216,7 +217,7 @@ private Location previousUseOfID(String idStr, Location location) { private static class Counter { int value = 1; } public ParserCIMXML_StAX_SR(XMLStreamReader2 reader, CimProfileRegistry cimProfileRegistry, String xmlBase, - StreamCIMXML destination, ErrorHandler errorHandler) { + StreamCimXml destination, ErrorHandler errorHandler) { // Debug IndentedWriter out = IndentedWriter.stdout.clone(); out.setFlushOnNewline(true); @@ -405,7 +406,7 @@ void parse() { // Not necessary to track this element when parsing. hasFrame = startElement(); // Only the XML base and namespaces that apply throughout rdf:RDF are parser output. - emitInitialBaseAndNamespacesDetermineCIMVersionAndSetBaseIfNeeded(); + emitInitialBaseAndNamespacesDetermineCimNamespaceAndSetBaseIfNeeded(); hasRDF = true; eventType = nextEventTag(); } @@ -1399,7 +1400,7 @@ private int nextEventAny() { if ( lookingAt(ev, PROCESSING_INSTRUCTION) ) { if (PROCESSING_INSTRUCTION_IEC61970_552.equals(xmlSource.getPITarget())) { final var versionOfIEC61970_552 = xmlSource.getPIData(); - destination.setVersionOfIEC61970_552(versionOfIEC61970_552); + destination.setVersionOfIec61970_552(versionOfIEC61970_552); } else { RDFXMLparseWarning("XML Processing instruction - ignored"); } @@ -1482,7 +1483,7 @@ private void endElement(boolean hasFrame) { // ---- Parser output - private void emitInitialBaseAndNamespacesDetermineCIMVersionAndSetBaseIfNeeded() { + private void emitInitialBaseAndNamespacesDetermineCimNamespaceAndSetBaseIfNeeded() { int numNS = xmlSource.getNamespaceCount(); for ( int i = 0 ; i < numNS ; i++ ) { @@ -1492,9 +1493,9 @@ private void emitInitialBaseAndNamespacesDetermineCIMVersionAndSetBaseIfNeeded() prefix = ""; } else if ("cim".equals(prefix)) { // Determine CIM version. - versionOfCIMXML = CimVersion.fromCimNamespace(prefixURI); - if (versionOfCIMXML == CimVersion.NO_CIM) { - RDFXMLparseWarning("Unrecognized 'cim' namespace: " + prefixURI, location()); + versionOfCIMXML = prefixURI; + if (!CimNamespaceFactoryRegistry.hasProfileFactory(prefixURI)) { + RDFXMLparseWarning("The provided 'cim' namespace: " + prefixURI + " is not registered in CimNamespaceMapper.", location()); } if (ReaderCIMXML_StAX_SR.TRACE) { trace.printf("CIM version of CIMXML: %s\n", versionOfCIMXML); @@ -1504,9 +1505,9 @@ private void emitInitialBaseAndNamespacesDetermineCIMVersionAndSetBaseIfNeeded() } String xmlBase = attribute(xmlQNameBase); - if (versionOfCIMXML != CimVersion.NO_CIM) { + if (versionOfCIMXML != null) { hasCimXmlNamespace = true; - destination.setVersionOfCIMXML(versionOfCIMXML); + destination.setCimNamespace(versionOfCIMXML); if (xmlBase == null) { xmlBase = xmlBaseForCIMXML; currentBase = IRIx.create(xmlBase); diff --git a/cimxml/src/main/java/de/soptim/opencgmes/cimxml/parser/RdfXmlParser.java b/cimxml/src/main/java/de/soptim/opencgmes/cimxml/parser/RdfXmlParser.java index a8f2f2f2..2a68ee4f 100644 --- a/cimxml/src/main/java/de/soptim/opencgmes/cimxml/parser/RdfXmlParser.java +++ b/cimxml/src/main/java/de/soptim/opencgmes/cimxml/parser/RdfXmlParser.java @@ -19,135 +19,140 @@ package de.soptim.opencgmes.cimxml.parser; import de.soptim.opencgmes.cimxml.graph.CimProfile; -import de.soptim.opencgmes.cimxml.parser.system.StreamCIMXMLToDatasetGraph; -import org.apache.commons.io.input.BufferedFileChannelInputStream; -import org.apache.jena.graph.Graph; -import org.apache.jena.riot.system.ErrorHandler; -import org.apache.jena.riot.system.ErrorHandlerFactory; - +import de.soptim.opencgmes.cimxml.parser.system.StreamCimXmlToDatasetGraph; import java.io.IOException; import java.io.InputStream; import java.io.Reader; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardOpenOption; +import org.apache.commons.io.input.BufferedFileChannelInputStream; +import org.apache.jena.graph.Graph; +import org.apache.jena.riot.system.ErrorHandler; +import org.apache.jena.riot.system.ErrorHandlerFactory; /** - * RDF/XML parser. - * This implementation uses ReaderCIMXML_StAX_SR, which is based on the RDF/XML reader ReaderRDFXML_StAX_SR - * in Apache Jena, originally. - * It has been adapted to the CIMXML needs. - *

    - * This implementation uses StAX via {@link javax.xml.stream.XMLStreamReader}. + * RDF/XML parser. This implementation uses ReaderCIMXML_StAX_SR, which is based on the RDF/XML + * reader ReaderRDFXML_StAX_SR in Apache Jena, originally. It has been adapted to the CIMXML needs. + * + *

    This implementation uses StAX via {@link javax.xml.stream.XMLStreamReader}. * - * @see https://webstore.iec.ch/en/publication/25939 + * @see https://webstore.iec.ch/en/publication/25939 */ public class RdfXmlParser { - final static int MAX_BUFFER_SIZE = 64*4096; - - private final ReaderCIMXML_StAX_SR reader; - - /** - * Gets the error handler used by this parser. - * @return the error handler - */ - public ErrorHandler getErrorHandler() { - return reader.errorHandler; - } - - /** - * Creates a new RDF/XML parser with the standard error handler. - */ - public RdfXmlParser() { - this(ErrorHandlerFactory.errorHandlerStd); - } - - /** - * Creates a new RDF/XML parser with the given reader. - * @param reader the reader to use - */ - RdfXmlParser(final ReaderCIMXML_StAX_SR reader) { - this.reader = reader; + static final int MAX_BUFFER_SIZE = 64 * 4096; + + private final ReaderCIMXML_StAX_SR reader; + + /** + * Creates a new RDF/XML parser with the standard error handler. + */ + public RdfXmlParser() { + this(ErrorHandlerFactory.errorHandlerStd); + } + + /** + * Creates a new RDF/XML parser with the given reader. + * + * @param reader the reader to use + */ + RdfXmlParser(final ReaderCIMXML_StAX_SR reader) { + this.reader = reader; + } + + /** + * Creates a new RDF/XML parser with the given error handler. + * + * @param errorHandler the error handler to use + */ + public RdfXmlParser(final ErrorHandler errorHandler) { + this(new ReaderCIMXML_StAX_SR(errorHandler)); + } + + /** + * Gets the error handler used by this parser. + * + * @return the error handler + */ + public ErrorHandler getErrorHandler() { + return reader.errorHandler; + } + + /** + * Parses the RDF/XML from the given reader and returns the resulting CIM profile. + * + * @param reader the reader containing RDF/XML data + * @return the resulting CIM profile + */ + public CimProfile parseCimProfile(final Reader reader) { + return CimProfile.wrap(parseGraph(reader)); + } + + /** + * Parses the RDF/XML from the given input stream and returns the resulting CIM profile. + * + * @param inputStream the input stream containing RDF/XML data + * @return the resulting CIM profile + */ + public CimProfile parseCimProfile(final InputStream inputStream) { + return CimProfile.wrap(parseGraph(inputStream)); + } + + /** + * Parses the RDF/XML file at the given path and returns the resulting CIM profile. + * + * @param pathToCimProfile the path to the RDF/XML file + * @return the resulting CIM profile + * @throws IOException if an I/O error occurs + */ + public CimProfile parseCimProfile(final Path pathToCimProfile) throws IOException { + return CimProfile.wrap(parseGraph(pathToCimProfile)); + } + + /** + * Parses the RDF/XML from the given reader and returns the resulting graph. + * + * @param reader the reader containing RDF/XML data + * @return the resulting graph + */ + public Graph parseGraph(final Reader reader) { + final var streamRdfProfile = new StreamCimXmlToDatasetGraph(); + this.reader.read(reader, streamRdfProfile); + return streamRdfProfile.getCimDatasetGraph().getDefaultGraph(); + } + + /** + * Parses the RDF/XML from the given input stream and returns the resulting graph. + * + * @param inputStream the input stream containing RDF/XML data + * @return the resulting graph + */ + public Graph parseGraph(final InputStream inputStream) { + final var streamRdfProfile = new StreamCimXmlToDatasetGraph(); + this.reader.read(inputStream, streamRdfProfile); + return streamRdfProfile.getCimDatasetGraph().getDefaultGraph(); + } + + /** + * Parses the RDF/XML file at the given path and returns the resulting graph. + * + * @param rdfxmlFilePath the path to the RDF/XML file + * @return the resulting graph + * @throws IOException if an I/O error occurs + */ + public Graph parseGraph(final Path rdfxmlFilePath) throws IOException { + final var fileSize = Files.size(rdfxmlFilePath); + final var streamRdfProfile = new StreamCimXmlToDatasetGraph(); + try (var is = new BufferedFileChannelInputStream.Builder() + .setPath(rdfxmlFilePath) + .setOpenOptions(StandardOpenOption.READ) + .setBufferSize((fileSize > MAX_BUFFER_SIZE) ? MAX_BUFFER_SIZE : (int) fileSize) + .get()) { + reader.read(is, streamRdfProfile); } - - /** - * Creates a new RDF/XML parser with the given error handler. - * @param errorHandler the error handler to use - */ - public RdfXmlParser(final ErrorHandler errorHandler) { - this(new ReaderCIMXML_StAX_SR(errorHandler)); - } - - /** - * Parses the RDF/XML from the given reader and returns the resulting CIM profile. - * @param reader the reader containing RDF/XML data - * @return the resulting CIM profile - */ - public CimProfile parseCimProfile(final Reader reader) { - return CimProfile.wrap(parseGraph(reader)); - } - - /** - * Parses the RDF/XML from the given input stream and returns the resulting CIM profile. - * @param inputStream the input stream containing RDF/XML data - * @return the resulting CIM profile - */ - public CimProfile parseCimProfile(final InputStream inputStream) { - return CimProfile.wrap(parseGraph(inputStream)); - } - - /** - * Parses the RDF/XML file at the given path and returns the resulting CIM profile. - * @param pathToCimProfile the path to the RDF/XML file - * @return the resulting CIM profile - * @throws IOException if an I/O error occurs - */ - public CimProfile parseCimProfile(final Path pathToCimProfile) throws IOException { - return CimProfile.wrap(parseGraph(pathToCimProfile)); - } - - /** - * Parses the RDF/XML from the given reader and returns the resulting graph. - * @param reader the reader containing RDF/XML data - * @return the resulting graph - */ - public Graph parseGraph(final Reader reader) { - final var streamRDFProfile = new StreamCIMXMLToDatasetGraph(); - this.reader.read(reader, streamRDFProfile); - return streamRDFProfile.getCIMDatasetGraph().getDefaultGraph(); - } - - /** - * Parses the RDF/XML from the given input stream and returns the resulting graph. - * @param inputStream the input stream containing RDF/XML data - * @return the resulting graph - */ - public Graph parseGraph(final InputStream inputStream) { - final var streamRDFProfile = new StreamCIMXMLToDatasetGraph(); - this.reader.read(inputStream, streamRDFProfile); - return streamRDFProfile.getCIMDatasetGraph().getDefaultGraph(); - } - - /** - * Parses the RDF/XML file at the given path and returns the resulting graph. - * @param rdfxmlFilePath the path to the RDF/XML file - * @return the resulting graph - * @throws IOException if an I/O error occurs - */ - public Graph parseGraph(final Path rdfxmlFilePath) throws IOException { - final var fileSize = Files.size(rdfxmlFilePath); - final var streamRDFProfile = new StreamCIMXMLToDatasetGraph(); - try(final var is = new BufferedFileChannelInputStream.Builder() - .setPath(rdfxmlFilePath) - .setOpenOptions(StandardOpenOption.READ) - .setBufferSize((fileSize > MAX_BUFFER_SIZE) ? MAX_BUFFER_SIZE : (int) fileSize) - .get()) { - reader.read(is, streamRDFProfile); - } - return streamRDFProfile.getCIMDatasetGraph().getDefaultGraph(); - } - - + return streamRdfProfile.getCimDatasetGraph().getDefaultGraph(); + } } diff --git a/cimxml/src/main/java/de/soptim/opencgmes/cimxml/parser/ReaderCIMXML_StAX_SR.java b/cimxml/src/main/java/de/soptim/opencgmes/cimxml/parser/ReaderCIMXML_StAX_SR.java index d56ca5a1..f74eb882 100644 --- a/cimxml/src/main/java/de/soptim/opencgmes/cimxml/parser/ReaderCIMXML_StAX_SR.java +++ b/cimxml/src/main/java/de/soptim/opencgmes/cimxml/parser/ReaderCIMXML_StAX_SR.java @@ -18,7 +18,7 @@ package de.soptim.opencgmes.cimxml.parser; -import de.soptim.opencgmes.cimxml.parser.system.StreamCIMXML; +import de.soptim.opencgmes.cimxml.parser.system.StreamCimXml; import de.soptim.opencgmes.cimxml.rdfs.CimProfileRegistry; import org.apache.jena.riot.RiotException; import org.apache.jena.riot.lang.rdfxml.SysRRX; @@ -41,6 +41,7 @@ * * @see https://webstore.iec.ch/en/publication/25939 */ +@SuppressWarnings("PMD") public class ReaderCIMXML_StAX_SR { public static XMLInputFactory2 createXMLInputFactory() { @@ -63,19 +64,19 @@ public ReaderCIMXML_StAX_SR(ErrorHandler errorHandler) { this.errorHandler = errorHandler; } - public void read(InputStream input, StreamCIMXML output) { + public void read(InputStream input, StreamCimXml output) { read(input, null, null, output); } - public void read(InputStream input, CimProfileRegistry cimProfileRegistry, StreamCIMXML output) { + public void read(InputStream input, CimProfileRegistry cimProfileRegistry, StreamCimXml output) { read(input, cimProfileRegistry, null, output); } - public void read(InputStream input, String xmlBase, StreamCIMXML output) { + public void read(InputStream input, String xmlBase, StreamCimXml output) { read(input, null, xmlBase, output); } - public void read(InputStream input, CimProfileRegistry cimProfileRegistry, String xmlBase, StreamCIMXML output) { + public void read(InputStream input, CimProfileRegistry cimProfileRegistry, String xmlBase, StreamCimXml output) { try { var xmlStreamReader = (XMLStreamReader2) xmlInputFactory.createXMLStreamReader(input); parse(xmlStreamReader, cimProfileRegistry, xmlBase, output); @@ -84,19 +85,19 @@ public void read(InputStream input, CimProfileRegistry cimProfileRegistry, Strin } } - public void read(Reader reader, StreamCIMXML output) { + public void read(Reader reader, StreamCimXml output) { read(reader, null, null, output); } - public void read(Reader reader, String xmlBase, StreamCIMXML output) { + public void read(Reader reader, String xmlBase, StreamCimXml output) { read(reader, null, xmlBase, output); } - public void read(Reader reader, CimProfileRegistry cimProfileRegistry, StreamCIMXML output) { + public void read(Reader reader, CimProfileRegistry cimProfileRegistry, StreamCimXml output) { read(reader, cimProfileRegistry, null, output); } - public void read(Reader reader, CimProfileRegistry cimProfileRegistry, String xmlBase, StreamCIMXML output) { + public void read(Reader reader, CimProfileRegistry cimProfileRegistry, String xmlBase, StreamCimXml output) { try { var xmlStreamReader = (XMLStreamReader2) xmlInputFactory.createXMLStreamReader(reader); parse(xmlStreamReader, cimProfileRegistry, xmlBase, output); @@ -106,7 +107,7 @@ public void read(Reader reader, CimProfileRegistry cimProfileRegistry, String xm } private void parse(XMLStreamReader2 xmlStreamReader, CimProfileRegistry cimProfileRegistry, String xmlBase, - StreamCIMXML destination) { + StreamCimXml destination) { var parser = new ParserCIMXML_StAX_SR(xmlStreamReader, cimProfileRegistry, xmlBase, destination, errorHandler); destination.start(); try { diff --git a/cimxml/src/main/java/de/soptim/opencgmes/cimxml/parser/system/StreamCIMXML.java b/cimxml/src/main/java/de/soptim/opencgmes/cimxml/parser/system/StreamCIMXML.java deleted file mode 100644 index 59167a9f..00000000 --- a/cimxml/src/main/java/de/soptim/opencgmes/cimxml/parser/system/StreamCIMXML.java +++ /dev/null @@ -1,77 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package de.soptim.opencgmes.cimxml.parser.system; - -import de.soptim.opencgmes.cimxml.CimVersion; -import de.soptim.opencgmes.cimxml.CimXmlDocumentContext; -import de.soptim.opencgmes.cimxml.graph.CimModelHeader; -import de.soptim.opencgmes.cimxml.sparql.core.CimDatasetGraph; -import org.apache.jena.riot.system.StreamRDF; - -/** - * An extension of {@link StreamRDF} to provide access to the underlying {@link CimDatasetGraph} - * and to store additional information about the CIMXML file being processed. - */ -public interface StreamCIMXML extends StreamRDF { - - /** - * Gets the underlying {@link CimDatasetGraph} that is being populated - */ - CimDatasetGraph getCIMDatasetGraph(); - - /** - * Gets the model header information as found in the CIMXML file - */ - CimModelHeader getModelHeader(); - - /** - * Sets the preprocessing instruction version of IEC 61970-552 - * @param versionOfCIMXML the version string as found in the CIMXML file - */ - void setVersionOfIEC61970_552(String versionOfCIMXML); - - /** - * Gets the preprocessing instruction version of IEC 61970-552 - * @return the version string as found in the CIMXML file - */ - String getVersionOfIEC61970_552(); - - /** - * Gets the CIMXMLVersion enum value for the given version string - * @return the CIMXMLVersion enum value - */ - CimVersion getVersionOfCIMXML(); - - /** - * Sets the CIMXMLVersion enum value for the given version string - * @param versionOfCIMXML the CIMXMLVersion enum value - */ - void setVersionOfCIMXML(CimVersion versionOfCIMXML); - - /** - * Gets the current document context - */ - CimXmlDocumentContext getCurrentContext(); - - /** - * Sets the current document context - * @param context the current document context - */ - void setCurrentContext(CimXmlDocumentContext context); -} diff --git a/cimxml/src/main/java/de/soptim/opencgmes/cimxml/parser/system/StreamCIMXMLToDatasetGraph.java b/cimxml/src/main/java/de/soptim/opencgmes/cimxml/parser/system/StreamCIMXMLToDatasetGraph.java deleted file mode 100644 index 38503142..00000000 --- a/cimxml/src/main/java/de/soptim/opencgmes/cimxml/parser/system/StreamCIMXMLToDatasetGraph.java +++ /dev/null @@ -1,161 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package de.soptim.opencgmes.cimxml.parser.system; - -import de.soptim.opencgmes.cimxml.CimVersion; -import de.soptim.opencgmes.cimxml.CimXmlDocumentContext; -import de.soptim.opencgmes.cimxml.graph.CimModelHeader; -import de.soptim.opencgmes.cimxml.sparql.core.CimDatasetGraph; -import de.soptim.opencgmes.cimxml.sparql.core.LinkedCimDatasetGraph; -import org.apache.jena.graph.Graph; -import org.apache.jena.graph.Node; -import org.apache.jena.graph.Triple; -import org.apache.jena.mem2.GraphMem2Roaring; -import org.apache.jena.mem2.IndexingStrategy; -import org.apache.jena.sparql.core.Quad; - -/** - * An implementation of {@link StreamCIMXML} that populates a {@link LinkedCimDatasetGraph} - * with the triples from the CIMXML file being processed. - *

    - * This class manages multiple named graphs within the dataset, switching the current graph - * context based on the {@link CimXmlDocumentContext}. It uses different indexing strategies - * for different contexts to optimize performance. - */ -public class StreamCIMXMLToDatasetGraph implements StreamCIMXML { - - private final LinkedCimDatasetGraph linkedCIMDatasetGraph; - private String versionOfIEC61970_552 = null; - private Graph currentGraph; - private CimXmlDocumentContext currentContext; - private CimVersion versionOfCIMXML = CimVersion.NO_CIM; - - public StreamCIMXMLToDatasetGraph() { - // init default graph for body context - currentContext = CimXmlDocumentContext.body; - currentGraph = new GraphMem2Roaring(IndexingStrategy.LAZY_PARALLEL); - linkedCIMDatasetGraph = new LinkedCimDatasetGraph(currentGraph); - } - - @Override - public String getVersionOfIEC61970_552() { - return versionOfIEC61970_552; - } - - @Override - public CimVersion getVersionOfCIMXML() { - return versionOfCIMXML; - } - - @Override - public void setVersionOfCIMXML(CimVersion versionOfCIMXML) { - this.versionOfCIMXML = versionOfCIMXML; - } - - @Override - public CimDatasetGraph getCIMDatasetGraph() { - return linkedCIMDatasetGraph; - } - - private void setCurrentGraphAndCreateIfNecessary(Node graphName, IndexingStrategy indexingStrategy) { - if (linkedCIMDatasetGraph.containsGraph(graphName)) { - currentGraph = linkedCIMDatasetGraph.getGraph(graphName); - } else { - final var newGraph = new GraphMem2Roaring(indexingStrategy); - newGraph.getPrefixMapping().setNsPrefixes(currentGraph.getPrefixMapping()); - currentGraph = newGraph; - linkedCIMDatasetGraph.addGraph(graphName, currentGraph); - } - } - - @Override - public void start() { - // Nothing to do - } - - @Override - public void triple(Triple triple) { - currentGraph.add(triple); - } - - @Override - public void quad(Quad quad) { - throw new UnsupportedOperationException("Quads are not supported in this context."); - } - - @Override - public void base(String base) { - // Nothing to do - } - - @Override - public void prefix(String prefix, String iri) { - linkedCIMDatasetGraph.prefixes().add(prefix, iri); - currentGraph.getPrefixMapping().setNsPrefix(prefix, iri); - } - - @Override - public void finish() { - // Initialize indexes in parallel for all graphs that use LAZY_PARALLEL indexing strategy. - linkedCIMDatasetGraph.getGraphs().parallelStream().forEach(graph -> { - if (graph instanceof GraphMem2Roaring roaring && !roaring.isIndexInitialized()) { - roaring.initializeIndexParallel(); - } - }); - } - - @Override - public CimModelHeader getModelHeader() { - return linkedCIMDatasetGraph.getModelHeader(); - } - - @Override - public void setVersionOfIEC61970_552(String versionOfIEC61970_552) { - this.versionOfIEC61970_552 = versionOfIEC61970_552; - } - - @Override - public CimXmlDocumentContext getCurrentContext() { - return currentContext; - } - - @Override - public void setCurrentContext(CimXmlDocumentContext context) { - switchContext(context); - } - - /** - * Switches the current graph context based on the provided {@link CimXmlDocumentContext}. - * This method updates the current graph to the appropriate named graph in the dataset, - * creating it if it does not already exist. The indexing strategy is chosen based on the - * context to optimize performance for different types of data. - * @param cimDocumentContext the new document context to switch to - */ - private void switchContext(CimXmlDocumentContext cimDocumentContext) { - var indexingStrategy = switch (cimDocumentContext) { - // The metadata is usually very small, so we use a minimal indexing strategy. - case fullModel, differenceModel -> IndexingStrategy.MINIMAL; - // The data parts can be large, so we use a lazy parallel indexing strategy. - default -> IndexingStrategy.LAZY_PARALLEL; - }; - var graphName = CimXmlDocumentContext.getGraphName(cimDocumentContext); - setCurrentGraphAndCreateIfNecessary(graphName, indexingStrategy); - currentContext = cimDocumentContext; - } -} diff --git a/cimxml/src/main/java/de/soptim/opencgmes/cimxml/parser/system/StreamCimXml.java b/cimxml/src/main/java/de/soptim/opencgmes/cimxml/parser/system/StreamCimXml.java new file mode 100644 index 00000000..bf48adc4 --- /dev/null +++ b/cimxml/src/main/java/de/soptim/opencgmes/cimxml/parser/system/StreamCimXml.java @@ -0,0 +1,81 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package de.soptim.opencgmes.cimxml.parser.system; + +import de.soptim.opencgmes.cimxml.CimXmlDocumentContext; +import de.soptim.opencgmes.cimxml.graph.CimModelHeader; +import de.soptim.opencgmes.cimxml.sparql.core.CimDatasetGraph; +import org.apache.jena.riot.system.StreamRDF; + +/** + * An extension of {@link StreamRDF} to provide access to the underlying {@link CimDatasetGraph} and + * to store additional information about the CIMXML file being processed. + */ +public interface StreamCimXml extends StreamRDF { + + /** + * Gets the underlying {@link CimDatasetGraph} that is being populated. + */ + CimDatasetGraph getCimDatasetGraph(); + + /** + * Gets the model header information as found in the CIMXML file. + */ + CimModelHeader getModelHeader(); + + /** + * Gets the preprocessing instruction version of IEC 61970-552. + * + * @return the version string as found in the CIMXML file + */ + String getVersionOfIec61970_552(); + + /** + * Sets the preprocessing instruction version of IEC 61970-552. + * + * @param versionOfCimXml the version string as found in the CIMXML file + */ + void setVersionOfIec61970_552(String versionOfCimXml); + + /** + * Gets the 'cim' namespace URI as found in the CIMXML file. + * + * @return the 'cim' namespace as found in the CIMXML file + */ + String getCimNamespace(); + + /** + * Sets the 'cim' namespace URI as found in the CIMXML file. + * + * @param cimNamespace the 'cim' namespace URI as found in the CIMXML file + */ + void setCimNamespace(String cimNamespace); + + /** + * Gets the current document context. + */ + CimXmlDocumentContext getCurrentContext(); + + /** + * Sets the current document context. + * + * @param context the current document context + */ + void setCurrentContext(CimXmlDocumentContext context); +} diff --git a/cimxml/src/main/java/de/soptim/opencgmes/cimxml/parser/system/StreamCimXmlToDatasetGraph.java b/cimxml/src/main/java/de/soptim/opencgmes/cimxml/parser/system/StreamCimXmlToDatasetGraph.java new file mode 100644 index 00000000..77d88310 --- /dev/null +++ b/cimxml/src/main/java/de/soptim/opencgmes/cimxml/parser/system/StreamCimXmlToDatasetGraph.java @@ -0,0 +1,153 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package de.soptim.opencgmes.cimxml.parser.system; + +import de.soptim.opencgmes.cimxml.CimXmlDocumentContext; +import de.soptim.opencgmes.cimxml.graph.CimModelHeader; +import de.soptim.opencgmes.cimxml.sparql.core.CimDatasetGraph; +import de.soptim.opencgmes.cimxml.sparql.core.LinkedCimDatasetGraph; +import org.apache.jena.graph.Graph; +import org.apache.jena.graph.Node; +import org.apache.jena.graph.Triple; +import org.apache.jena.sparql.core.Quad; +import org.apache.jena.sparql.graph.GraphFactory; + +/** + * An implementation of {@link StreamCimXml} that populates a {@link LinkedCimDatasetGraph} with the + * triples from the CIMXML file being processed. + * + *

    This class manages multiple named graphs within the dataset, switching the current graph + * context based on the {@link CimXmlDocumentContext}. It uses different indexing strategies for + * different contexts to optimize performance. + */ +public class StreamCimXmlToDatasetGraph implements StreamCimXml { + + private final LinkedCimDatasetGraph linkedCimDatasetGraph; + private String versionOfIec61970_552 = null; + private Graph currentGraph; + private CimXmlDocumentContext currentContext; + private String cimNamespace = null; + + /** + * Creates a new instance of {@link StreamCimXmlToDatasetGraph} and initializes the default graph + * for the body context. The default graph is created as an in-memory graph and is associated with + * the body context in the linked dataset graph. + */ + public StreamCimXmlToDatasetGraph() { + // init default graph for body context + currentContext = CimXmlDocumentContext.body; + currentGraph = GraphFactory.createGraphMem(); + linkedCimDatasetGraph = new LinkedCimDatasetGraph(currentGraph); + } + + @Override + public String getVersionOfIec61970_552() { + return versionOfIec61970_552; + } + + @Override + public void setVersionOfIec61970_552(String versionOfCimXml) { + this.versionOfIec61970_552 = versionOfCimXml; + } + + @Override + public String getCimNamespace() { + return cimNamespace; + } + + @Override + public void setCimNamespace(String cimNamespace) { + this.cimNamespace = cimNamespace; + } + + @Override + public CimDatasetGraph getCimDatasetGraph() { + return linkedCimDatasetGraph; + } + + private void setCurrentGraphAndCreateIfNecessary(Node graphName) { + if (linkedCimDatasetGraph.containsGraph(graphName)) { + currentGraph = linkedCimDatasetGraph.getGraph(graphName); + } else { + final Graph newGraph = GraphFactory.createGraphMem(); + newGraph.getPrefixMapping().setNsPrefixes(currentGraph.getPrefixMapping()); + currentGraph = newGraph; + linkedCimDatasetGraph.addGraph(graphName, currentGraph); + } + } + + @Override + public void start() { + // Nothing to do + } + + @Override + public void triple(Triple triple) { + currentGraph.add(triple); + } + + @Override + public void quad(Quad quad) { + throw new UnsupportedOperationException("Quads are not supported in this context."); + } + + @Override + public void base(String base) { + // Nothing to do + } + + @Override + public void prefix(String prefix, String iri) { + linkedCimDatasetGraph.prefixes().add(prefix, iri); + currentGraph.getPrefixMapping().setNsPrefix(prefix, iri); + } + + @Override + public void finish() { + // Nothing to do + } + + @Override + public CimModelHeader getModelHeader() { + return linkedCimDatasetGraph.getModelHeader(); + } + + @Override + public CimXmlDocumentContext getCurrentContext() { + return currentContext; + } + + @Override + public void setCurrentContext(CimXmlDocumentContext context) { + switchContext(context); + } + + /** + * Switches the current graph context based on the provided {@link CimXmlDocumentContext}. This + * method updates the current graph to the appropriate named graph in the dataset, creating it if + * it does not already exist. + * + * @param cimDocumentContext the new document context to switch to + */ + private void switchContext(CimXmlDocumentContext cimDocumentContext) { + var graphName = CimXmlDocumentContext.getGraphName(cimDocumentContext); + setCurrentGraphAndCreateIfNecessary(graphName); + currentContext = cimDocumentContext; + } +} diff --git a/cimxml/src/main/java/de/soptim/opencgmes/cimxml/rdfs/CimProfileRegistry.java b/cimxml/src/main/java/de/soptim/opencgmes/cimxml/rdfs/CimProfileRegistry.java index 3eec9ae0..94c191ce 100644 --- a/cimxml/src/main/java/de/soptim/opencgmes/cimxml/rdfs/CimProfileRegistry.java +++ b/cimxml/src/main/java/de/soptim/opencgmes/cimxml/rdfs/CimProfileRegistry.java @@ -18,13 +18,11 @@ package de.soptim.opencgmes.cimxml.rdfs; -import de.soptim.opencgmes.cimxml.CimVersion; import de.soptim.opencgmes.cimxml.graph.CimProfile; -import org.apache.jena.datatypes.RDFDatatype; -import org.apache.jena.graph.Node; - import java.util.Map; import java.util.Set; +import org.apache.jena.datatypes.RDFDatatype; +import org.apache.jena.graph.Node; /** * Registry for managing CIM profile ontologies and their associated datatypes. @@ -67,6 +65,7 @@ * } * *

    Thread Safety:

    + * *

    Implementations must be thread-safe for all read operations. Registration operations * may require external synchronization.

    * @@ -75,96 +74,118 @@ */ public interface CimProfileRegistry { - /** - * Information about a CIM property including its domain, range, and datatype. - * - *

    This record encapsulates all metadata needed to properly parse and validate - * a CIM property value:

    - * - *
      - *
    • rdfType: The class (domain) this property belongs to
    • - *
    • property: The property URI
    • - *
    • cimDatatype: The CIM datatype definition
    • - *
    • primitiveType: RDF datatype for primitive properties (e.g., xsd:float)
    • - *
    • referenceType: Target class for object properties
    • - *
    - * - *

    Either primitiveType or referenceType will be non-null, but not both:

    - *
      - *
    • If primitiveType is non-null, this is a datatype property
    • - *
    • If referenceType is non-null, this is an object property
    • - *
    - * - * @param rdfType The domain class of this property - * @param property The property URI - * @param cimDatatype The CIM datatype definition (may be null) - * @param primitiveType The RDF datatype for primitive values (may be null) - * @param referenceType The range class for object properties (may be null) - */ - record PropertyInfo(Node rdfType, Node property, Node cimDatatype, RDFDatatype primitiveType, Node referenceType) {} + /** + * Registers an ontology graph for profiles in the registry. During registration, the data types + * of all properties in the graph are extracted and stored in a map for fast lookup. Throws an + * IllegalArgumentException if one of the profiles owlVersionIRIs is already registered or in case + * of a header profile, if one has already been registered for the same CIM version. + * + * @param cimProfile The profile ontology to register. + */ + void register(CimProfile cimProfile); + + /** + * Checks if the registry contains all profile IRIs in the given set. + * + * @param owlVersionIris A set of profile IRIs as found in the model header. + * @return true if all profile IRIs are registered, false otherwise. + */ + boolean containsProfile(Set owlVersionIris); + + /** + * Get all registered profiles that match the given set of profile IRIs. + * + * @param owlVersionIris A set of profile IRIs as found in the model header. + * @return A set of matching profiles. If no profiles match, an empty set is returned. + */ + Set getMatchingProfiles(Set owlVersionIris); - /** - * Registers an ontology graph for profiles in the registry. - * During registration, the data types of all properties in the graph are extracted and stored in a map for fast lookup. - * Throws an IllegalArgumentException if one of the profiles owlVersionIRIs is already registered - * or in case of a header profile, if one has already been registered for the same CIM version. - * @param cimProfile The profile ontology to register. - */ - void register(CimProfile cimProfile); + /** + * Checks if the registry contains a header profile for the given CIM namespace. + * + * @param cimNamespace The 'cim' namespace to check. + * @return true if a header profile for the given 'cim' namespace is registered, false otherwise. + */ + boolean containsHeaderProfile(String cimNamespace); - /** - * Checks if the registry contains all profile IRIs in the given set. - * @param owlVersionIRIs A set of profile IRIs as found in the model header. - * @return true if all profile IRIs are registered, false otherwise. - */ - boolean containsProfile(Set owlVersionIRIs); + /** + * Get all registered ontologies in the registry. + * + * @return A collection of all registered ontologies. + */ + Set getRegisteredProfiles(); - /** - * Checks if the registry contains a header profile for the given CIM version. - * @param version The CIM version to check. - * @return true if a header profile for the given CIM version is registered, false otherwise. - */ - boolean containsHeaderProfile(CimVersion version); + /** + * Get all properties and their associated RDF datatypes for the given set of profile IRIs. The + * set may contain profile IRIs for multiple ontologies. Throws an IllegalArgumentException if one + * of the profile IRIs is not registered. + * + * @param owlVersionIris A set of profile IRIs as found in the model header. + * @return A map of properties and their associated RDF datatypes. The map is thread-safe for + * reading. Returns an empty map if one of the profile IRIs is not registered. + */ + Map getPropertiesAndDatatypes(Set owlVersionIris); - /** - * Get all registered ontologies in the registry. - * @return A collection of all registered ontologies. - */ - Set getRegisteredProfiles(); + /** + * Get all properties and their associated RDF datatypes for the header profile of the given CIM + * version. Throws an IllegalArgumentException if no header profile has been registered for the + * given CIM version. + * + * @param cimNamespace The 'cim' namespace for which the header profile should be used. + * @return A map of properties and their associated RDF datatypes. The map is thread-safe for + * reading. Returns an empty map if no header profile is registered for the given 'cim' + * namespace. + */ + Map getHeaderPropertiesAndDatatypes(String cimNamespace); - /** - * Get all properties and their associated RDF datatypes for the given set of profile IRIs. - * The set may contain profile IRIs for multiple ontologies. - * Throws an IllegalArgumentException if one of the profile IRIs is not registered. - * @param owlVersionIRIs A set of profile IRIs as found in the model header. - * @return A map of properties and their associated RDF datatypes. The map is thread-safe for reading. - * Returns null if one of the profile IRIs is not registered. - */ - Map getPropertiesAndDatatypes(Set owlVersionIRIs); + /** + * Get a mapping of primitive type names to RDF datatypes for all registered profiles. This + * includes primitive types from all registered ontologies. + * + * @return A map of primitive type names to RDF datatypes. The map is thread-safe for reading. + */ + Map getPrimitiveToRdfDatatypeMapping(); - /** - * Get all properties and their associated RDF datatypes for the header profile of the given CIM version. - * Throws an IllegalArgumentException if no header profile has been registered for the given CIM version. - * @param version The CIM version for which the header profile should be used. - * @return A map of properties and their associated RDF datatypes. The map is thread-safe for reading. - * Returns null if no header profile is registered for the given CIM version. - */ - Map getHeaderPropertiesAndDatatypes(CimVersion version); + /** + * Registers a custom primitive type with the given CIM primitive type name and RDF datatype. If + * the primitive type name is already registered, it will be overwritten with the new RDF + * datatype. This method can be used to register custom primitive types that are not part of the + * standard CIM profiles. The rdfDatatype must also be registered with Jena's TypeMapper. + * + * @param cimPrimitiveTypeName The CIM primitive type name, e.g. "string", "int", "float", etc. + * @param rdfDatatype The RDF datatype to associate with the given CIM primitive type + * name. + */ + void registerPrimitiveType(String cimPrimitiveTypeName, RDFDatatype rdfDatatype); - /** - * Get a mapping of primitive type names to RDF datatypes for all registered profiles. - * This includes primitive types from all registered ontologies. - * @return A map of primitive type names to RDF datatypes. The map is thread-safe for reading. - */ - Map getPrimitiveToRDFDatatypeMapping(); + /** + * Information about a CIM property including its domain, range, and datatype. + * + *

    This record encapsulates all metadata needed to properly parse and validate + * a CIM property value:

    + * + *
      + *
    • rdfType: The class (domain) this property belongs to
    • + *
    • property: The property URI
    • + *
    • cimDatatype: The CIM datatype definition
    • + *
    • primitiveType: RDF datatype for primitive properties (e.g., xsd:float)
    • + *
    • referenceType: Target class for object properties
    • + *
    + * + *

    Either primitiveType or referenceType will be non-null, but not both:

    + *
      + *
    • If primitiveType is non-null, this is a datatype property
    • + *
    • If referenceType is non-null, this is an object property
    • + *
    + * + * @param rdfType The domain class of this property + * @param property The property URI + * @param cimDatatype The CIM datatype definition (may be null) + * @param primitiveType The RDF datatype for primitive values (may be null) + * @param referenceType The range class for object properties (may be null) + */ + record PropertyInfo(Node rdfType, Node property, Node cimDatatype, RDFDatatype primitiveType, + Node referenceType) { - /** - * Registers a custom primitive type with the given CIM primitive type name and RDF datatype. - * If the primitive type name is already registered, it will be overwritten with the new RDF datatype. - * This method can be used to register custom primitive types that are not part of the standard CIM profiles. - * The rdfDatatype must also be registered with Jena's TypeMapper. - * @param cimPrimitiveTypeName The CIM primitive type name, e.g. "string", "int", "float", etc. - * @param rdfDatatype The RDF datatype to associate with the given CIM primitive type name. - */ - void registerPrimitiveType(String cimPrimitiveTypeName, RDFDatatype rdfDatatype); + } } diff --git a/cimxml/src/main/java/de/soptim/opencgmes/cimxml/rdfs/CimProfileRegistryStd.java b/cimxml/src/main/java/de/soptim/opencgmes/cimxml/rdfs/CimProfileRegistryStd.java index 866873f2..97c65933 100644 --- a/cimxml/src/main/java/de/soptim/opencgmes/cimxml/rdfs/CimProfileRegistryStd.java +++ b/cimxml/src/main/java/de/soptim/opencgmes/cimxml/rdfs/CimProfileRegistryStd.java @@ -18,8 +18,14 @@ package de.soptim.opencgmes.cimxml.rdfs; -import de.soptim.opencgmes.cimxml.CimVersion; import de.soptim.opencgmes.cimxml.graph.CimProfile; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; import org.apache.jena.datatypes.RDFDatatype; import org.apache.jena.datatypes.xsd.XSDDatatype; import org.apache.jena.datatypes.xsd.impl.RDFLangString; @@ -31,288 +37,311 @@ import org.apache.jena.riot.system.ErrorHandlerFactory; import org.apache.jena.sparql.exec.QueryExec; -import java.util.*; -import java.util.concurrent.ConcurrentHashMap; - /** - * Standard implementation of the {@link CimProfileRegistry}. - * This implementation is thread-safe. - * Registration of custom primitive type mappings should be done before any other operations on the registry. - * The primitive type mapping is static for all instances of the registry. + * Standard implementation of the {@link CimProfileRegistry}. This implementation is thread-safe. + * Registration of custom primitive type mappings should be done before any other operations on the + * registry. The primitive type mapping is static for all instances of the registry. */ public class CimProfileRegistryStd implements CimProfileRegistry { + private static final Query typedPropertiesQuery = QueryFactory.create( + """ + PREFIX rdf: + PREFIX rdfs: + PREFIX cims: + + SELECT ?rdfType ?property ?cimDatatype ?primitiveType ?referenceType + WHERE + { + { + ?property rdfs:domain ?rdfType; + rdfs:range ?referenceType; + OPTIONAL { + ?property cims:AssociationUsed ?associationUsed + } + FILTER(!BOUND(?associationUsed) || ?associationUsed = "Yes") + } + UNION + { + ?property rdfs:domain ?rdfType; + cims:dataType ?cimDatatype. + { + ?cimDatatype cims:stereotype "CIMDatatype". + [] rdfs:domain ?cimDatatype; + rdfs:label ?label; + #rdfs:label "value"; + cims:dataType/cims:stereotype "Primitive"; + cims:dataType/rdfs:label ?primitiveType. + FILTER (!bound(?label) || str(?label) = "value") + } + UNION + { + ?cimDatatype cims:stereotype "Primitive"; + rdfs:label ?primitiveType. + } + } + } + """); + private final ErrorHandler errorHandler; + private final Map, CimProfile> multiVersionIriProfiles = new ConcurrentHashMap<>(); + private final Map singleVersionIriProfiles = new ConcurrentHashMap<>(); + private final Map headerProfilesByCimNamespace = new ConcurrentHashMap<>(); + private final Map> profilePropertiesCache + = new ConcurrentHashMap<>(); + private final Map, Map> profileSetPropertiesCache + = new ConcurrentHashMap<>(); + private final Map primitiveToRdfDatatypeMap; - private static Map initPrimitiveToRDFDatatypeMapUsingXSDDatatypesOnly() { - var map = new HashMap(); - map.put("Base64Binary", XSDDatatype.XSDbase64Binary); - map.put("Boolean", XSDDatatype.XSDboolean); - map.put("Byte", XSDDatatype.XSDbyte); - map.put("Date", XSDDatatype.XSDdate); - map.put("DateTime", XSDDatatype.XSDdateTime); - map.put("DateTimeStamp", XSDDatatype.XSDdateTimeStamp); - map.put("Day", XSDDatatype.XSDgDay); - map.put("DayTimeDuration", XSDDatatype.XSDdayTimeDuration); - map.put("Decimal", XSDDatatype.XSDdecimal); - map.put("Double", XSDDatatype.XSDdouble); - map.put("Duration", XSDDatatype.XSDduration); - map.put("Float", XSDDatatype.XSDfloat); - map.put("HexBinary", XSDDatatype.XSDhexBinary); - map.put("Int", XSDDatatype.XSDint); - map.put("Integer", XSDDatatype.XSDinteger); - map.put("IRI", XSDDatatype.XSDstring); - map.put("LangString", RDFLangString.rdfLangString); - map.put("Long", XSDDatatype.XSDlong); - map.put("Month", XSDDatatype.XSDgMonth); - map.put("MonthDay", XSDDatatype.XSDgMonthDay); - map.put("NegativeInteger", XSDDatatype.XSDnegativeInteger); - map.put("NonNegativeInteger", XSDDatatype.XSDnonNegativeInteger); - map.put("NonPositiveInteger", XSDDatatype.XSDnonPositiveInteger); - map.put("PositiveInteger", XSDDatatype.XSDpositiveInteger); - map.put("String", XSDDatatype.XSDstring); - map.put("StringFixedLanguage", XSDDatatype.XSDstring); - map.put("StringIRI", XSDDatatype.XSDstring); - map.put("Time", XSDDatatype.XSDtime); - map.put("UnsignedByte", XSDDatatype.XSDunsignedByte); - map.put("UnsignedInt", XSDDatatype.XSDunsignedInt); - map.put("UnsignedLong", XSDDatatype.XSDunsignedLong); - map.put("UnsignedShort", XSDDatatype.XSDunsignedShort); - map.put("URI", XSDDatatype.XSDanyURI); - map.put("UUID", XSDDatatype.XSDstring); - map.put("Version", XSDDatatype.XSDstring); - map.put("Year", XSDDatatype.XSDgYear); - map.put("YearMonth", XSDDatatype.XSDgYearMonth); - map.put("YearMonthDuration", XSDDatatype.XSDyearMonthDuration); - return map; - } + /** + * Creates a new instance of the registry using the standard Jena error handler. + */ + public CimProfileRegistryStd() { + this(ErrorHandlerFactory.errorHandlerStd); + } - private final Map, CimProfile> multiVersionIriProfiles = new ConcurrentHashMap<>(); - private final Map singleVersionIriProfiles = new ConcurrentHashMap<>(); - private final Map headerProfiles = new ConcurrentHashMap<>(); - private final Map> profilePropertiesCache = new ConcurrentHashMap<>(); - private final Map, Map> profileSetPropertiesCache = new ConcurrentHashMap<>(); - private final Map primitiveToRDFDatatypeMap; + /** + * Creates a new instance of the registry using the given error handler. + * + * @param errorHandler The error handler to use for logging warnings and errors. + */ + public CimProfileRegistryStd(ErrorHandler errorHandler) { + this.errorHandler = errorHandler; + this.primitiveToRdfDatatypeMap = initPrimitiveToRdfDatatypeMapUsingXsdDatatypesOnly(); + } - public final ErrorHandler errorHandler; + private static Map initPrimitiveToRdfDatatypeMapUsingXsdDatatypesOnly() { + var map = new ConcurrentHashMap(); + map.put("Base64Binary", XSDDatatype.XSDbase64Binary); + map.put("Boolean", XSDDatatype.XSDboolean); + map.put("Byte", XSDDatatype.XSDbyte); + map.put("Date", XSDDatatype.XSDdate); + map.put("DateTime", XSDDatatype.XSDdateTime); + map.put("DateTimeStamp", XSDDatatype.XSDdateTimeStamp); + map.put("Day", XSDDatatype.XSDgDay); + map.put("DayTimeDuration", XSDDatatype.XSDdayTimeDuration); + map.put("Decimal", XSDDatatype.XSDdecimal); + map.put("Double", XSDDatatype.XSDdouble); + map.put("Duration", XSDDatatype.XSDduration); + map.put("Float", XSDDatatype.XSDfloat); + map.put("HexBinary", XSDDatatype.XSDhexBinary); + map.put("Int", XSDDatatype.XSDint); + map.put("Integer", XSDDatatype.XSDinteger); + map.put("IRI", XSDDatatype.XSDstring); + map.put("LangString", RDFLangString.rdfLangString); + map.put("Long", XSDDatatype.XSDlong); + map.put("Month", XSDDatatype.XSDgMonth); + map.put("MonthDay", XSDDatatype.XSDgMonthDay); + map.put("NegativeInteger", XSDDatatype.XSDnegativeInteger); + map.put("NonNegativeInteger", XSDDatatype.XSDnonNegativeInteger); + map.put("NonPositiveInteger", XSDDatatype.XSDnonPositiveInteger); + map.put("PositiveInteger", XSDDatatype.XSDpositiveInteger); + map.put("String", XSDDatatype.XSDstring); + map.put("StringFixedLanguage", XSDDatatype.XSDstring); + map.put("StringIRI", XSDDatatype.XSDstring); + map.put("Time", XSDDatatype.XSDtime); + map.put("UnsignedByte", XSDDatatype.XSDunsignedByte); + map.put("UnsignedInt", XSDDatatype.XSDunsignedInt); + map.put("UnsignedLong", XSDDatatype.XSDunsignedLong); + map.put("UnsignedShort", XSDDatatype.XSDunsignedShort); + map.put("URI", XSDDatatype.XSDanyURI); + map.put("UUID", XSDDatatype.XSDstring); + map.put("Version", XSDDatatype.XSDstring); + map.put("Year", XSDDatatype.XSDgYear); + map.put("YearMonth", XSDDatatype.XSDgYearMonth); + map.put("YearMonthDuration", XSDDatatype.XSDyearMonthDuration); + return map; + } - /** - * Creates a new instance of the registry using the standard Jena error handler. - */ - public CimProfileRegistryStd() { - this(ErrorHandlerFactory.errorHandlerStd); + @Override + public void register(CimProfile cimProfile) { + if (cimProfile.isHeaderProfile()) { + final var cimNamespace = cimProfile.getCimNamespace(); + if (cimNamespace == null) { + throw new IllegalArgumentException( + "Header profile must have the 'cim' prefix and namespace URI defined."); + } + if (headerProfilesByCimNamespace.containsKey(cimNamespace)) { + throw new IllegalArgumentException( + "Header profile for 'cim' namespace URI '" + cimNamespace + "' is already registered."); + } + headerProfilesByCimNamespace.put(cimNamespace, cimProfile); + profilePropertiesCache.put(cimProfile, getTypedProperties(cimProfile)); + return; } - /** - * Creates a new instance of the registry using the given error handler. - * @param errorHandler The error handler to use for logging warnings and errors. - */ - public CimProfileRegistryStd(ErrorHandler errorHandler) { - this.errorHandler = errorHandler; - this.primitiveToRDFDatatypeMap = initPrimitiveToRDFDatatypeMapUsingXSDDatatypesOnly(); + var owlVersionIris = cimProfile.getOwlVersionIris(); + if (owlVersionIris == null || owlVersionIris.isEmpty()) { + throw new IllegalArgumentException("Profile ontology must have at least one owlVersionIRI."); } - private final static Query typedPropertiesQuery = QueryFactory.create(""" - PREFIX rdf: - PREFIX rdfs: - PREFIX cims: - - SELECT ?rdfType ?property ?cimDatatype ?primitiveType ?referenceType - WHERE - { - { - ?property rdfs:domain ?rdfType; - rdfs:range ?referenceType; - OPTIONAL { - ?property cims:AssociationUsed ?associationUsed - } - FILTER(!BOUND(?associationUsed) || ?associationUsed = "Yes") - } - UNION - { - ?property rdfs:domain ?rdfType; - cims:dataType ?cimDatatype. - { - ?cimDatatype cims:stereotype "CIMDatatype". - [] rdfs:domain ?cimDatatype; - rdfs:label ?label; - #rdfs:label "value"; - cims:dataType/cims:stereotype "Primitive"; - cims:dataType/rdfs:label ?primitiveType. - FILTER (!bound(?label) || str(?label) = "value") - } - UNION - { - ?cimDatatype cims:stereotype "Primitive"; - rdfs:label ?primitiveType. - } - } - } - """); + if (owlVersionIris.size() == 1) { + var iri = owlVersionIris.iterator().next(); + if (singleVersionIriProfiles.containsKey(iri)) { + throw new IllegalArgumentException( + "Profile ontology with owlVersionIRI " + iri + " is already registered."); + } + singleVersionIriProfiles.put(iri, cimProfile); + } else { + if (multiVersionIriProfiles.containsKey(owlVersionIris)) { + throw new IllegalArgumentException( + "Profile ontology with owlVersionIris " + owlVersionIris + " is already registered."); + } + multiVersionIriProfiles.put(owlVersionIris, cimProfile); + } + profilePropertiesCache.put(cimProfile, getTypedProperties(cimProfile)); + } - @Override - public void register(CimProfile cimProfile) { - if (cimProfile.isHeaderProfile()) { - final var cimVersion = cimProfile.getCIMVersion(); - if (cimVersion == CimVersion.NO_CIM) - throw new IllegalArgumentException("Header profile must have a valid CIM version."); - if (headerProfiles.containsKey(cimVersion)) - throw new IllegalArgumentException("Header profile for CIM version " + cimVersion + " is already registered."); - headerProfiles.put(cimVersion, cimProfile); - profilePropertiesCache.put(cimProfile, getTypedProperties(cimProfile)); - return; + @Override + public boolean containsProfile(Set owlVersionIris) { + if (owlVersionIris == null || owlVersionIris.isEmpty()) { + throw new IllegalArgumentException("At least one profile owlVersionIRI must be provided."); + } + for (var iri : owlVersionIris) { + if (!singleVersionIriProfiles.containsKey(iri)) { + var foundInMulti = false; + for (var registeredVersionIris : multiVersionIriProfiles.keySet()) { + if (registeredVersionIris.contains(iri)) { + foundInMulti = true; + break; + } } - - var owlVersionIRIs = cimProfile.getOwlVersionIRIs(); - if (owlVersionIRIs == null || owlVersionIRIs.isEmpty()) - throw new IllegalArgumentException("Profile ontology must have at least one owlVersionIRI."); - - if (owlVersionIRIs.size() == 1) { - var iri = owlVersionIRIs.iterator().next(); - if (singleVersionIriProfiles.containsKey(iri)) - throw new IllegalArgumentException("Profile ontology with owlVersionIRI " + iri + " is already registered."); - singleVersionIriProfiles.put(iri, cimProfile); - } else { - if (multiVersionIriProfiles.containsKey(owlVersionIRIs)) - throw new IllegalArgumentException("Profile ontology with owlVersionIRIs " + owlVersionIRIs + " is already registered."); - multiVersionIriProfiles.put(owlVersionIRIs, cimProfile); + if (!foundInMulti) { + return false; } - profilePropertiesCache.put(cimProfile, getTypedProperties(cimProfile)); + } } + return true; + } - @Override - public boolean containsProfile(Set owlVersionIRIs) { - if (owlVersionIRIs == null || owlVersionIRIs.isEmpty()) - throw new IllegalArgumentException("At least one profile owlVersionIRI must be provided."); - for(var iri : owlVersionIRIs) { - if (!singleVersionIriProfiles.containsKey(iri)) { - var foundInMulti = false; - for(var registeredVersionIRIs: multiVersionIriProfiles.keySet()) { - if (registeredVersionIRIs.contains(iri)) { - foundInMulti = true; - break; - } - } - if (!foundInMulti) - return false; - } - } - return true; + @Override + public Set getMatchingProfiles(Set owlVersionIris) { + if (owlVersionIris == null || owlVersionIris.isEmpty()) { + throw new IllegalArgumentException("At least one profile owlVersionIRI must be provided."); } - @Override - public boolean containsHeaderProfile(CimVersion version) { - if (version == CimVersion.NO_CIM) - throw new IllegalArgumentException("CIM version must be valid."); - return headerProfiles.containsKey(version); + if (owlVersionIris.size() == 1) { + var versionIri = owlVersionIris.iterator().next(); + if (singleVersionIriProfiles.containsKey(versionIri)) { + return Set.of(singleVersionIriProfiles.get(versionIri)); + } } - @Override - public Set getRegisteredProfiles() { - return profilePropertiesCache.keySet(); + var profile = multiVersionIriProfiles.get(owlVersionIris); + if (profile != null) { + return Set.of(profile); } - @Override - public Map getPropertiesAndDatatypes(Set owlVersionIRIs) { - if (owlVersionIRIs == null || owlVersionIRIs.isEmpty()) - throw new IllegalArgumentException("At least one profile owlVersionIRI must be provided."); - - if (owlVersionIRIs.size() == 1) { - var versionIRI = owlVersionIRIs.iterator().next(); - if (singleVersionIriProfiles.containsKey(versionIRI)) { - var profile = singleVersionIriProfiles.get(versionIRI); - return profilePropertiesCache.get(profile); - } + final var set = new HashSet(); + for (var owlVersionIri : owlVersionIris) { + final var p = singleVersionIriProfiles.get(owlVersionIri); + if (p == null) { + var foundInMulti = false; + for (var entrySet : multiVersionIriProfiles.entrySet()) { + if (entrySet.getKey().contains(owlVersionIri)) { + foundInMulti = true; + set.add(entrySet.getValue()); + break; + } } - - var profile = multiVersionIriProfiles.get(owlVersionIRIs); - if (profile != null) - return profilePropertiesCache.get(profile); - - var set = new HashSet(); - for(var owlVersionIRI : owlVersionIRIs) { - final var p = singleVersionIriProfiles.get(owlVersionIRI); - if (p == null) { - var foundInMulti = false; - for (var entrySet : multiVersionIriProfiles.entrySet()) { - if (entrySet.getKey().contains(owlVersionIRI)) { - foundInMulti = true; - set.add(entrySet.getValue()); - break; - } - } - if (!foundInMulti) - return null; - } else { - set.add(p); - } + if (!foundInMulti) { + return Collections.emptySet(); } - if (set.size() == 1) - return profilePropertiesCache.get(set.iterator().next()); + } else { + set.add(p); + } + } + return set; + } - Map properties = profileSetPropertiesCache.get(set); - if (properties != null) - return properties; + @Override + public boolean containsHeaderProfile(String cimNamespace) { + Objects.requireNonNull(cimNamespace, "cimNamespace"); + return headerProfilesByCimNamespace.containsKey(cimNamespace); + } - properties = new HashMap<>(1024); - for(var p : set) { - properties.putAll(profilePropertiesCache.get(p)); - } - properties = Collections.unmodifiableMap(properties); - profileSetPropertiesCache.put(set, properties); - return properties; + @Override + public Set getRegisteredProfiles() { + return profilePropertiesCache.keySet(); + } + + @Override + public Map getPropertiesAndDatatypes(Set owlVersionIris) { + if (owlVersionIris == null || owlVersionIris.isEmpty()) { + throw new IllegalArgumentException("At least one profile owlVersionIRI must be provided."); } - @Override - public Map getHeaderPropertiesAndDatatypes(CimVersion version) { - Objects.requireNonNull(version, "version"); - if (version == CimVersion.NO_CIM) - throw new IllegalArgumentException("CIM version must be valid."); - final var profile = headerProfiles.get(version); - if (profile == null) - return null; - return profilePropertiesCache.get(profile); + final var set = getMatchingProfiles(owlVersionIris); + + if (set.size() == 1) { + return profilePropertiesCache.get(set.iterator().next()); } - @Override - public Map getPrimitiveToRDFDatatypeMapping() { - return Collections.unmodifiableMap(primitiveToRDFDatatypeMap); + Map properties = profileSetPropertiesCache.get(set); + if (properties != null) { + return properties; } - @Override - public void registerPrimitiveType(String cimPrimitiveTypeName, RDFDatatype rdfDatatype) { - Objects.requireNonNull(cimPrimitiveTypeName, "cimPrimitiveTypeName"); - Objects.requireNonNull(rdfDatatype, "rdfDatatype"); - primitiveToRDFDatatypeMap.put(cimPrimitiveTypeName, rdfDatatype); + properties = new HashMap<>(1024); + for (var p : set) { + properties.putAll(profilePropertiesCache.get(p)); } + properties = Collections.unmodifiableMap(properties); + profileSetPropertiesCache.put(set, properties); + return properties; + } - private Map getTypedProperties(Graph g) { - final var map = new HashMap(1024); - QueryExec.graph(g) - .query(typedPropertiesQuery) - .select() - .forEachRemaining(vars -> { //?class ?property ?primitiveType ?referenceType - final var rdfType = vars.get("rdfType"); - final var property = vars.get("property"); - final var cimDatatype = vars.get("cimDatatype"); - final var primitiveType = vars.get("primitiveType"); - final var referenceType = vars.get("referenceType"); - map.put(property, new PropertyInfo( - rdfType, - property, - cimDatatype, - primitiveType != null - ? getXsdDatatype(primitiveType.getLiteralLexicalForm()) - : null, - referenceType)); - }); - return Collections.unmodifiableMap(map); + @Override + public Map getHeaderPropertiesAndDatatypes(String cimNamespace) { + Objects.requireNonNull(cimNamespace, "cimNamespace"); + final var profile = headerProfilesByCimNamespace.get(cimNamespace); + if (profile == null) { + return Collections.emptyMap(); } + return profilePropertiesCache.get(profile); + } + + @Override + public Map getPrimitiveToRdfDatatypeMapping() { + return Collections.unmodifiableMap(primitiveToRdfDatatypeMap); + } + + @Override + public void registerPrimitiveType(String cimPrimitiveTypeName, RDFDatatype rdfDatatype) { + Objects.requireNonNull(cimPrimitiveTypeName, "cimPrimitiveTypeName"); + Objects.requireNonNull(rdfDatatype, "rdfDatatype"); + primitiveToRdfDatatypeMap.put(cimPrimitiveTypeName, rdfDatatype); + } + + private Map getTypedProperties(Graph g) { + final var map = new HashMap(1024); + QueryExec.graph(g) + .query(typedPropertiesQuery) + .select() + .forEachRemaining(vars -> { // ?class ?property ?primitiveType ?referenceType + final var rdfType = vars.get("rdfType"); + final var property = vars.get("property"); + final var cimDatatype = vars.get("cimDatatype"); + final var primitiveType = vars.get("primitiveType"); + final var referenceType = vars.get("referenceType"); + map.put(property, new PropertyInfo( + rdfType, + property, + cimDatatype, + primitiveType != null + ? getXsdDatatype(primitiveType.getLiteralLexicalForm()) + : null, + referenceType)); + }); + return Collections.unmodifiableMap(map); + } - private RDFDatatype getXsdDatatype(String primitiveType) { - var dt = primitiveToRDFDatatypeMap.get(primitiveType); - if (dt != null) - return dt; - errorHandler.warning("Unknown mapping from CIM primitive'" + primitiveType + "' to XSD datatype. Using xsd:string as fallback.", -1,-1); - return XSDDatatype.XSDstring; + private RDFDatatype getXsdDatatype(String primitiveType) { + var dt = primitiveToRdfDatatypeMap.get(primitiveType); + if (dt != null) { + return dt; } + errorHandler.warning("Unknown mapping from CIM primitive'" + primitiveType + + "' to XSD datatype. Using xsd:string as fallback.", -1, -1); + return XSDDatatype.XSDstring; + } } diff --git a/cimxml/src/main/java/de/soptim/opencgmes/cimxml/sparql/core/CimDatasetGraph.java b/cimxml/src/main/java/de/soptim/opencgmes/cimxml/sparql/core/CimDatasetGraph.java index f1d7a213..74b4a4b8 100644 --- a/cimxml/src/main/java/de/soptim/opencgmes/cimxml/sparql/core/CimDatasetGraph.java +++ b/cimxml/src/main/java/de/soptim/opencgmes/cimxml/sparql/core/CimDatasetGraph.java @@ -22,18 +22,17 @@ import de.soptim.opencgmes.cimxml.graph.CimModelHeader; import de.soptim.opencgmes.cimxml.graph.DisjointMultiUnion; import de.soptim.opencgmes.cimxml.graph.FastDeltaGraph; +import java.util.ArrayList; import org.apache.jena.graph.Graph; import org.apache.jena.graph.Triple; import org.apache.jena.sparql.core.DatasetGraph; -import java.util.ArrayList; - /** * A specialized {@link DatasetGraph} for IEC 61970-552 CIM models. * *

    This interface extends the standard Jena DatasetGraph with CIM-specific operations - * for handling the structure defined in IEC 61970-552. A CIM dataset can contain either - * a FullModel or a DifferenceModel:

    + * for handling the structure defined in IEC 61970-552. A CIM dataset can contain either a FullModel + * or a DifferenceModel:

    * *

    FullModel Structure:

    *
      @@ -77,141 +76,180 @@ */ public interface CimDatasetGraph extends DatasetGraph { - /** - * Checks if this dataset contains a FullModel. - * @return true if this dataset contains a FullModel, false otherwise - */ - default boolean isFullModel() { - return containsGraph(CimHeaderVocabulary.TYPE_FULL_MODEL); + /** + * Checks if this dataset contains a FullModel. + * + * @return true if this dataset contains a FullModel, false otherwise + */ + default boolean isFullModel() { + return containsGraph(CimHeaderVocabulary.TYPE_FULL_MODEL); + } + + /** + * Checks if this dataset contains a DifferenceModel. + * + * @return true if this dataset contains a DifferenceModel, false otherwise + */ + default boolean isDifferenceModel() { + return containsGraph(CimHeaderVocabulary.TYPE_DIFFERENCE_MODEL); + } + + /** + * Gets the forward differences graph of this DifferenceModel. + * + * @return the forward differences graph + * @throws IllegalStateException if this dataset is not a DifferenceModel + */ + default Graph getForwardDifferences() { + if (!this.isDifferenceModel()) { + throw new IllegalStateException( + "Forward differences are only available for DifferenceModels. Use isDifferenceModel()" + + " to check."); } - - /** - * Checks if this dataset contains a DifferenceModel. - * @return true if this dataset contains a DifferenceModel, false otherwise - */ - default boolean isDifferenceModel() { - return containsGraph(CimHeaderVocabulary.TYPE_DIFFERENCE_MODEL); + return getGraph(CimHeaderVocabulary.GRAPH_FORWARD_DIFFERENCES); + } + + /** + * Gets the reverse differences graph of this DifferenceModel. + * + * @return the reverse differences graph + * @throws IllegalStateException if this dataset is not a DifferenceModel + */ + default Graph getReverseDifferences() { + if (!this.isDifferenceModel()) { + throw new IllegalStateException( + "Reverse differences are only available for DifferenceModels. Use isDifferenceModel()" + + " to check."); } - - /** - * Gets the forward differences graph of this DifferenceModel. - * @return the forward differences graph - * @throws IllegalStateException if this dataset is not a DifferenceModel - */ - default Graph getForwardDifferences() { - if (!this.isDifferenceModel()) - throw new IllegalStateException("Forward differences are only available for DifferenceModels. Use isDifferenceModel() to check."); - return getGraph(CimHeaderVocabulary.GRAPH_FORWARD_DIFFERENCES); + return getGraph(CimHeaderVocabulary.GRAPH_REVERSE_DIFFERENCES); + } + + /** + * Gets the preconditions graph of this DifferenceModel. + * + * @return the preconditions graph + * @throws IllegalStateException if this dataset is not a DifferenceModel + */ + default Graph getPreconditions() { + if (!this.isDifferenceModel()) { + throw new IllegalStateException( + "Preconditions are only available for DifferenceModels. Use isDifferenceModel()" + + " to check."); } - - /** - * Gets the reverse differences graph of this DifferenceModel. - * @return the reverse differences graph - * @throws IllegalStateException if this dataset is not a DifferenceModel - */ - default Graph getReverseDifferences() { - if (!this.isDifferenceModel()) - throw new IllegalStateException("Reverse differences are only available for DifferenceModels. Use isDifferenceModel() to check."); - return getGraph(CimHeaderVocabulary.GRAPH_REVERSE_DIFFERENCES); + return getGraph(CimHeaderVocabulary.GRAPH_PRECONDITIONS); + } + + /** + * Gets the model header of this FullModel or DifferenceModel. + * + * @return the model header + * @throws IllegalStateException if this dataset is not a FullModel or DifferenceModel + */ + default CimModelHeader getModelHeader() { + var graphName = isFullModel() + ? CimHeaderVocabulary.TYPE_FULL_MODEL + : isDifferenceModel() + ? CimHeaderVocabulary.TYPE_DIFFERENCE_MODEL + : null; + + if (graphName == null) { + throw new IllegalStateException( + "Model header is only available for FullModels or DifferenceModels. Use isFullModel()" + + " or isDifferenceModel() to check."); } - /** - * Gets the preconditions graph of this DifferenceModel. - * @return the preconditions graph - * @throws IllegalStateException if this dataset is not a DifferenceModel - */ - default Graph getPreconditions() { - if (!this.isDifferenceModel()) - throw new IllegalStateException("Preconditions are only available for DifferenceModels. Use isDifferenceModel() to check."); - return getGraph(CimHeaderVocabulary.GRAPH_PRECONDITIONS); + return CimModelHeader.wrap(getGraph(graphName)); + } + + /** + * Gets the body graph of this FullModel. + * + * @return the body graph + * @throws IllegalStateException if this dataset is not a FullModel + */ + default Graph getBody() { + if (!this.isFullModel()) { + throw new IllegalStateException( + "Body graph is only available for FullModels. Use isFullModel() to check."); } - - /** - * Gets the model header of this FullModel or DifferenceModel. - * @return the model header - * @throws IllegalStateException if this dataset is not a FullModel or DifferenceModel - */ - default CimModelHeader getModelHeader() { - var graphName = isFullModel() - ? CimHeaderVocabulary.TYPE_FULL_MODEL - : isDifferenceModel() - ? CimHeaderVocabulary.TYPE_DIFFERENCE_MODEL - : null; - - if (graphName == null) - throw new IllegalStateException("Model header is only available for FullModels or DifferenceModels. Use isFullModel() or isDifferenceModel() to check."); - - return CimModelHeader.wrap(getGraph(graphName)); + return getDefaultGraph(); + } + + /** + * Converts this FullModel to a single graph by combining the model header and the body graph. + * + * @return a new Graph representing the FullModel - containing both the Model header and the body + * graph + * @throws IllegalStateException if this dataset is not a FullModel + */ + default Graph fullModelToSingleGraph() { + if (!this.isFullModel()) { + throw new IllegalStateException( + "Full model graph is only available for FullModels. Use isFullModel() to check."); } - /** - * Gets the body graph of this FullModel. - * @return the body graph - * @throws IllegalStateException if this dataset is not a FullModel - */ - default Graph getBody() { - if (!this.isFullModel()) - throw new IllegalStateException("Body graph is only available for FullModels. Use isFullModel() to check."); - return getDefaultGraph(); + var header = getModelHeader(); + var body = getBody(); + + var union = new DisjointMultiUnion(header, body); + union.getPrefixMapping().setNsPrefixes(header.getPrefixMapping()); + + return union; + } + + /** + * Converts this DifferenceModel to a FullModel by applying the forward and reverse differences to + * the provided predecessor FullModel. + * + * @param predecessorFullModel the predecessor FullModel to which the differences will be applied + * @return a new Graph representing the resulting FullModel - containing only the body graph + * @throws IllegalStateException if this dataset is not a DifferenceModel + * @throws IllegalArgumentException if the provided predecessorFullModel is not a FullModel, if + * its Model is not in the current Model.Supersedes, or if it + * does not contain all required preconditions + */ + default Graph differenceModelToFullModel(CimDatasetGraph predecessorFullModel) { + if (!this.isDifferenceModel()) { + throw new IllegalStateException( + "Conversion to full model is only available for DifferenceModels. Use isDifferenceModel()" + + " to check."); + } + if (!predecessorFullModel.isFullModel()) { + throw new IllegalArgumentException( + "The provided predecessorFullModel dataset must be a FullModel. Use isFullModel()" + + " to check."); } - /** - * Converts this FullModel to a single graph by combining the model header and the body graph. - * @return a new Graph representing the FullModel - containing both the Model header and the body graph - * @throws IllegalStateException if this dataset is not a FullModel - */ - default Graph fullModelToSingleGraph() { - if (!this.isFullModel()) - throw new IllegalStateException("Full model graph is only available for FullModels. Use isFullModel() to check."); - - var header = getModelHeader(); - var body = getBody(); - - var union = new DisjointMultiUnion(header, body); - union.getPrefixMapping().setNsPrefixes(header.getPrefixMapping()); - - return union; + if (this.getModelHeader().getSupersedes() + .contains(predecessorFullModel.getModelHeader().getModel())) { + throw new IllegalArgumentException( + "The provided predecessorFullModel dataset Model must be in current Model.Supersedes."); } - /** - * Converts this DifferenceModel to a FullModel by applying the forward and reverse differences - * to the provided predecessor FullModel. - * @param predecessorFullModel the predecessor FullModel to which the differences will be applied - * @return a new Graph representing the resulting FullModel - containing only the body graph - * @throws IllegalStateException if this dataset is not a DifferenceModel - * @throws IllegalArgumentException if the provided predecessorFullModel is not a FullModel, - * if its Model is not in the current Model.Supersedes, - * or if it does not contain all required preconditions - */ - default Graph differenceModelToFullModel(CimDatasetGraph predecessorFullModel) { - if (!this.isDifferenceModel()) - throw new IllegalStateException("Conversion to full model is only available for DifferenceModels. Use isDifferenceModel() to check."); - if (!predecessorFullModel.isFullModel()) - throw new IllegalArgumentException("The provided predecessorFullModel dataset must be a FullModel. Use isFullModel() to check."); - - if (this.getModelHeader().getSupersedes().contains(predecessorFullModel.getModelHeader().getModel())) - throw new IllegalArgumentException("The provided predecessorFullModel dataset Model must be in current Model.Supersedes."); - - var predecessorBody = predecessorFullModel.getBody(); - - var preconditions = getPreconditions(); - if (preconditions != null && !preconditions.isEmpty()) { - var missingPreconditions = new ArrayList(); - preconditions.find().forEachRemaining(t -> { - if (!predecessorBody.contains(t)) - missingPreconditions.add(t); - }); - if (!missingPreconditions.isEmpty()) { - throw new IllegalArgumentException("The provided predecessorFullModel dataset does not contain all required preconditions. Missing preconditions: " + missingPreconditions); - } + var predecessorBody = predecessorFullModel.getBody(); + + var preconditions = getPreconditions(); + if (preconditions != null && !preconditions.isEmpty()) { + var missingPreconditions = new ArrayList(); + preconditions.find().forEachRemaining(t -> { + if (!predecessorBody.contains(t)) { + missingPreconditions.add(t); } + }); + if (!missingPreconditions.isEmpty()) { + throw new IllegalArgumentException( + "The provided predecessorFullModel dataset does not contain all required preconditions." + + " Missing preconditions: " + + missingPreconditions); + } + } - var forwardDifferences = getForwardDifferences(); - var reverseDifferences = getReverseDifferences(); + var forwardDifferences = getForwardDifferences(); + var reverseDifferences = getReverseDifferences(); - var deltaGraph = new FastDeltaGraph(predecessorBody, forwardDifferences, reverseDifferences); - deltaGraph.getPrefixMapping().setNsPrefixes(this.getModelHeader().getPrefixMapping()); + var deltaGraph = new FastDeltaGraph(predecessorBody, forwardDifferences, reverseDifferences); + deltaGraph.getPrefixMapping().setNsPrefixes(this.getModelHeader().getPrefixMapping()); - return deltaGraph; - } + return deltaGraph; + } } diff --git a/cimxml/src/main/java/de/soptim/opencgmes/cimxml/sparql/core/LinkedCimDatasetGraph.java b/cimxml/src/main/java/de/soptim/opencgmes/cimxml/sparql/core/LinkedCimDatasetGraph.java index e0eaad21..73c4796e 100644 --- a/cimxml/src/main/java/de/soptim/opencgmes/cimxml/sparql/core/LinkedCimDatasetGraph.java +++ b/cimxml/src/main/java/de/soptim/opencgmes/cimxml/sparql/core/LinkedCimDatasetGraph.java @@ -18,190 +18,233 @@ package de.soptim.opencgmes.cimxml.sparql.core; +import java.io.Serial; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; import org.apache.jena.graph.Graph; import org.apache.jena.graph.Node; import org.apache.jena.query.ReadWrite; import org.apache.jena.query.TxnType; import org.apache.jena.riot.system.PrefixMap; import org.apache.jena.riot.system.PrefixMapStd; -import org.apache.jena.sparql.core.*; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.Iterator; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentMap; +import org.apache.jena.sparql.core.DatasetGraph; +import org.apache.jena.sparql.core.DatasetGraphCollection; +import org.apache.jena.sparql.core.Quad; +import org.apache.jena.sparql.core.Transactional; +import org.apache.jena.sparql.core.TransactionalLock; /** * A {@link DatasetGraph} that holds a set of named graphs and an optional default graph. - *

      - * This implementation provides "best effort" transactions; it only provides MRPlusSW locking. - * So all graphs that are transactional must support MRPlusSW locking. - *

      - * Only the transactional graphs are included in the transaction. + * + *

      This implementation provides "best effort" transactions; it only provides MRPlusSW locking. So + * all graphs that are transactional must support MRPlusSW locking. + * + *

      Only the transactional graphs are included in the transaction. */ public class LinkedCimDatasetGraph extends DatasetGraphCollection implements CimDatasetGraph { - public LinkedCimDatasetGraph() { - super(); - } - - public LinkedCimDatasetGraph(Graph defaultGraph) { - this(); - addGraph(Quad.defaultGraphIRI, defaultGraph); - } - - public static class LinkedDatasetTransactionException extends RuntimeException { - - private final Collection exceptions; - - public Collection getExceptions() { - return exceptions; - } - - public LinkedDatasetTransactionException(String message, Collection exceptions) { - super(message); - this.exceptions = exceptions; - } - } - - private final ConcurrentMap graphs = new ConcurrentHashMap<>(); - private final ConcurrentMap transactionalGraphs = new ConcurrentHashMap<>(); - - private final TransactionalLock txn = TransactionalLock.createMRPlusSW(); - - public Collection getGraphs() { - return graphs.values(); - } - - @Override - public Iterator listGraphNodes() { - return graphs.keySet().iterator(); - } - - protected final PrefixMap prefixes = new PrefixMapStd(); - - @Override - public PrefixMap prefixes() { - return prefixes; - } - - @Override - public boolean supportsTransactions() { - return true; - } - - @Override - public Graph getDefaultGraph() { - return graphs.getOrDefault(Quad.defaultGraphIRI, Graph.emptyGraph); - } - - @Override - public Graph getGraph(Node graphNode) { - return graphs.get(graphNode); - } - - @Override - public void addGraph(Node graphName, Graph graph) { - graphs.put(graphName, graph); - if (graph instanceof Transactional transactional) { - transactionalGraphs.put(graphName, transactional); - } - } - - @Override - public void removeGraph(Node graphName) { - graphs.remove(graphName); - transactionalGraphs.remove(graphName); - } - - @Override - public void begin(TxnType type) { - txn.begin(type); - var openedTransactions = new ArrayList(); - try { - for (Transactional transactional : transactionalGraphs.values()) { - transactional.begin(type); - openedTransactions.add(transactional); - } - } catch (Exception e) { - txn.abort(); - for (Transactional transactional : openedTransactions) { - transactional.abort(); - } - throw e; - } - } - - @Override - public boolean promote(Promote mode) { - return false; - } - - @Override - public void commit() { - txn.commit(); - var failedCommits = new ArrayList(); - for (Transactional transactional : transactionalGraphs.values()) { - try { - transactional.commit(); - } catch (Exception e) { - failedCommits.add(e); - } - } - if (!failedCommits.isEmpty()) { - //Exception with message "Failed to commit transactions on x graphs" - throw new LinkedDatasetTransactionException("Failed to commit transactions on " + failedCommits.size() + " graphs", failedCommits); - } - } - - @Override - public void abort() { - txn.abort(); - var failedAborts = new ArrayList(); - for (Transactional transactional : transactionalGraphs.values()) { - try { - transactional.abort(); - } catch (Exception e) { - failedAborts.add(e); - } - } - if (!failedAborts.isEmpty()) { - //Exception with message "Failed to abort transactions on x graphs" - throw new LinkedDatasetTransactionException("Failed to abort transactions on " + failedAborts.size() + " graphs", failedAborts); - } - } - - @Override - public void end() { - txn.end(); - var failedEnds = new ArrayList(); - for (Transactional transactional : transactionalGraphs.values()) { - try { - transactional.end(); - } catch (Exception e) { - failedEnds.add(e); - } - } - if (!failedEnds.isEmpty()) { - //Exception with message "Failed to end transaction on x graphs" - throw new LinkedDatasetTransactionException("Failed to end transaction on " + failedEnds.size() + " graphs", failedEnds); - } - } - - @Override - public ReadWrite transactionMode() { - return txn.transactionMode(); - } - - @Override - public TxnType transactionType() { - return txn.transactionType(); - } - - @Override - public boolean isInTransaction() { - return txn.isInTransaction(); - } + protected final PrefixMap prefixes = new PrefixMapStd(); + private final ConcurrentMap graphs = new ConcurrentHashMap<>(); + private final ConcurrentMap transactionalGraphs = new ConcurrentHashMap<>(); + private final TransactionalLock txn = TransactionalLock.createMRPlusSW(); + + /** + * Creates a new empty LinkedCimDatasetGraph with no default graph and no named graphs. + */ + public LinkedCimDatasetGraph() { + super(); + } + + /** + * Creates a new LinkedCimDatasetGraph with the given default graph and no named graphs. + * The defaultgraph is associated with the default graph name (Quad.defaultGraphIRI). + * + * @param defaultGraph the default graph to be used in this dataset + */ + public LinkedCimDatasetGraph(Graph defaultGraph) { + this(); + addGraph(graphs, transactionalGraphs, Quad.defaultGraphIRI, defaultGraph); + } + + public Collection getGraphs() { + return graphs.values(); + } + + @Override + public Iterator listGraphNodes() { + return graphs.keySet().iterator(); + } + + @Override + public PrefixMap prefixes() { + return prefixes; + } + + @Override + public boolean supportsTransactions() { + return true; + } + + @Override + public Graph getDefaultGraph() { + return graphs.getOrDefault(Quad.defaultGraphIRI, Graph.emptyGraph); + } + + @Override + public Graph getGraph(Node graphNode) { + return graphs.get(graphNode); + } + + private static void addGraph(Map namedGraphs, + Map namedTransactionals, + Node graphName, Graph graph) { + namedGraphs.put(graphName, graph); + if (graph instanceof Transactional transactional) { + namedTransactionals.put(graphName, transactional); + } + } + + @Override + public void addGraph(Node graphName, Graph graph) { + addGraph(graphs, transactionalGraphs, graphName, graph); + } + + @Override + public void removeGraph(Node graphName) { + graphs.remove(graphName); + transactionalGraphs.remove(graphName); + } + + @Override + public void begin(TxnType type) { + txn.begin(type); + var openedTransactions = new ArrayList(); + try { + for (Transactional transactional : transactionalGraphs.values()) { + transactional.begin(type); + openedTransactions.add(transactional); + } + } catch (Exception e) { + txn.abort(); + for (Transactional transactional : openedTransactions) { + transactional.abort(); + } + throw e; + } + } + + @Override + public boolean promote(Promote mode) { + return false; + } + + @Override + public void commit() { + txn.commit(); + var failedCommits = new ArrayList(); + for (Transactional transactional : transactionalGraphs.values()) { + try { + transactional.commit(); + } catch (Exception e) { + failedCommits.add(e); + } + } + if (!failedCommits.isEmpty()) { + // Exception with message "Failed to commit transactions on x graphs" + throw new LinkedDatasetTransactionException( + "Failed to commit transactions on " + failedCommits.size() + " graphs", failedCommits); + } + } + + @Override + public void abort() { + txn.abort(); + var failedAborts = new ArrayList(); + for (Transactional transactional : transactionalGraphs.values()) { + try { + transactional.abort(); + } catch (Exception e) { + failedAborts.add(e); + } + } + if (!failedAborts.isEmpty()) { + // Exception with message "Failed to abort transactions on x graphs" + throw new LinkedDatasetTransactionException( + "Failed to abort transactions on " + failedAborts.size() + " graphs", failedAborts); + } + } + + @Override + public void end() { + txn.end(); + var failedEnds = new ArrayList(); + for (Transactional transactional : transactionalGraphs.values()) { + try { + transactional.end(); + } catch (Exception e) { + failedEnds.add(e); + } + } + if (!failedEnds.isEmpty()) { + // Exception with message "Failed to end transaction on x graphs" + throw new LinkedDatasetTransactionException( + "Failed to end transaction on " + failedEnds.size() + " graphs", failedEnds); + } + } + + @Override + public ReadWrite transactionMode() { + return txn.transactionMode(); + } + + @Override + public TxnType transactionType() { + return txn.transactionType(); + } + + @Override + public boolean isInTransaction() { + return txn.isInTransaction(); + } + + /** + * Exception thrown when one or more transactional graphs fail to commit, abort, + * or end a transaction. + * This exception contains a collection of the individual exceptions that occurred + * during the transaction operation. + */ + public static class LinkedDatasetTransactionException extends RuntimeException { + + @Serial + private static final long serialVersionUID = 1L; + + private final ArrayList exceptions; + + /** + * Creates a new LinkedDatasetTransactionException with the given message + * and collection of exceptions. + * + * @param message the detail message for this exception + * @param exceptions the collection of exceptions that occurred during the transaction operation + */ + public LinkedDatasetTransactionException(String message, Collection exceptions) { + super(message); + this.exceptions = new ArrayList<>(exceptions); + } + + /** + * Returns the collection of exceptions that occurred during the transaction operation. + * + * @return the collection of exceptions + */ + public List getExceptions() { + return exceptions; + } + } } diff --git a/cimxml/src/test/java/de/soptim/opencgmes/cimxml/graph/TestCimModelHeader.java b/cimxml/src/test/java/de/soptim/opencgmes/cimxml/graph/TestCimModelHeader.java index 78fe7e19..5da8d2ac 100644 --- a/cimxml/src/test/java/de/soptim/opencgmes/cimxml/graph/TestCimModelHeader.java +++ b/cimxml/src/test/java/de/soptim/opencgmes/cimxml/graph/TestCimModelHeader.java @@ -19,8 +19,9 @@ package de.soptim.opencgmes.cimxml.graph; import de.soptim.opencgmes.cimxml.parser.ReaderCIMXML_StAX_SR; -import de.soptim.opencgmes.cimxml.parser.system.StreamCIMXMLToDatasetGraph; +import de.soptim.opencgmes.cimxml.parser.system.StreamCimXmlToDatasetGraph; import org.apache.jena.graph.Node; +import org.apache.jena.riot.system.ErrorHandlerFactory; import org.junit.Test; import java.io.StringReader; @@ -52,14 +53,14 @@ public void parseFullModelHeader() { """; - final var parser = new ReaderCIMXML_StAX_SR(); - final var streamRDF = new StreamCIMXMLToDatasetGraph(); + final var parser = new ReaderCIMXML_StAX_SR(ErrorHandlerFactory.errorHandlerNoWarnings); + final var streamRDF = new StreamCimXmlToDatasetGraph(); parser.read(new StringReader(rdfxml), streamRDF); - assertTrue(streamRDF.getCIMDatasetGraph().isFullModel()); - assertNotNull(streamRDF.getCIMDatasetGraph().getModelHeader()); - var modelHeader = streamRDF.getCIMDatasetGraph().getModelHeader(); + assertTrue(streamRDF.getCimDatasetGraph().isFullModel()); + assertNotNull(streamRDF.getCimDatasetGraph().getModelHeader()); + var modelHeader = streamRDF.getCimDatasetGraph().getModelHeader(); assertEquals("urn:uuid:08984e27-811f-4042-9125-1531ae0de0f6", modelHeader.getModel().toString()); @@ -102,14 +103,14 @@ public void parseDifferenceModelHeader() { """; - final var parser = new ReaderCIMXML_StAX_SR(); - final var streamRDF = new StreamCIMXMLToDatasetGraph(); + final var parser = new ReaderCIMXML_StAX_SR(ErrorHandlerFactory.errorHandlerNoWarnings); + final var streamRDF = new StreamCimXmlToDatasetGraph(); parser.read(new StringReader(rdfxml), streamRDF); - assertTrue(streamRDF.getCIMDatasetGraph().isDifferenceModel()); - assertNotNull(streamRDF.getCIMDatasetGraph().getModelHeader()); - var modelHeader = streamRDF.getCIMDatasetGraph().getModelHeader(); + assertTrue(streamRDF.getCimDatasetGraph().isDifferenceModel()); + assertNotNull(streamRDF.getCimDatasetGraph().getModelHeader()); + var modelHeader = streamRDF.getCimDatasetGraph().getModelHeader(); assertEquals("urn:uuid:08984e27-811f-4042-9125-1531ae0de0f6", modelHeader.getModel().toString()); diff --git a/cimxml/src/test/java/de/soptim/opencgmes/cimxml/graph/TestCimProfile16.java b/cimxml/src/test/java/de/soptim/opencgmes/cimxml/graph/TestCimProfile16.java index cfa3d3c4..6fc463fe 100644 --- a/cimxml/src/test/java/de/soptim/opencgmes/cimxml/graph/TestCimProfile16.java +++ b/cimxml/src/test/java/de/soptim/opencgmes/cimxml/graph/TestCimProfile16.java @@ -18,9 +18,8 @@ package de.soptim.opencgmes.cimxml.graph; -import de.soptim.opencgmes.cimxml.CimVersion; -import org.apache.jena.mem2.GraphMem2Roaring; import org.apache.jena.riot.RDFParser; +import org.apache.jena.sparql.graph.GraphFactory; import org.junit.Test; import java.io.StringReader; @@ -118,7 +117,7 @@ public void parseProfileOntologyHeader() { """; - var graph = new GraphMem2Roaring(); + var graph = GraphFactory.createGraphMem(); RDFParser.create() .source(new StringReader(rdfxml)) @@ -129,20 +128,20 @@ public void parseProfileOntologyHeader() { var ontology = CimProfile.wrap(graph); assertFalse(ontology.isHeaderProfile()); - assertEquals(CimVersion.CIM_16, ontology.getCIMVersion()); + assertEquals(CimProfile16.CIM_NAMESPACE, ontology.getCimNamespace()); - assertEquals(6, ontology.getOwlVersionIRIs().size()); - assertTrue(ontology.getOwlVersionIRIs().stream() + assertEquals(6, ontology.getOwlVersionIris().size()); + assertTrue(ontology.getOwlVersionIris().stream() .anyMatch(n -> n.getURI().equals("http://example.org/MyCustom/Core/1/1"))); - assertTrue(ontology.getOwlVersionIRIs().stream() + assertTrue(ontology.getOwlVersionIris().stream() .anyMatch(n -> n.getURI().equals("http://example.org/MyCustom/Operation/1/1"))); - assertTrue(ontology.getOwlVersionIRIs().stream() + assertTrue(ontology.getOwlVersionIris().stream() .anyMatch(n -> n.getURI().equals("http://example.org/MyCustom/ShortCircuit/1/1"))); - assertTrue(ontology.getOwlVersionIRIs().stream() + assertTrue(ontology.getOwlVersionIris().stream() .anyMatch(n -> n.getURI().equals("http://entsoe.eu/CIM/MyCustomCore/2/2"))); - assertTrue(ontology.getOwlVersionIRIs().stream() + assertTrue(ontology.getOwlVersionIris().stream() .anyMatch(n -> n.getURI().equals("http://entsoe.eu/CIM/MyCustomOperation/2/2"))); - assertTrue(ontology.getOwlVersionIRIs().stream() + assertTrue(ontology.getOwlVersionIris().stream() .anyMatch(n -> n.getURI().equals("http://entsoe.eu/CIM/MyCustomShortCircuit/2/2"))); assertNull(ontology.getOwlVersionInfo()); @@ -164,7 +163,7 @@ public void parseProfileFileHeaderProfile() { """; - var graph = new GraphMem2Roaring(); + var graph = GraphFactory.createGraphMem(); RDFParser.create() .source(new StringReader(rdfxml)) @@ -175,6 +174,6 @@ public void parseProfileFileHeaderProfile() { var ontology = CimProfile.wrap(graph); assertTrue(ontology.isHeaderProfile()); - assertEquals(CimVersion.CIM_16, ontology.getCIMVersion()); + assertEquals(CimProfile16.CIM_NAMESPACE, ontology.getCimNamespace()); } } diff --git a/cimxml/src/test/java/de/soptim/opencgmes/cimxml/graph/TestCimProfile17.java b/cimxml/src/test/java/de/soptim/opencgmes/cimxml/graph/TestCimProfile17.java index 92175f89..3fd78467 100644 --- a/cimxml/src/test/java/de/soptim/opencgmes/cimxml/graph/TestCimProfile17.java +++ b/cimxml/src/test/java/de/soptim/opencgmes/cimxml/graph/TestCimProfile17.java @@ -18,13 +18,11 @@ package de.soptim.opencgmes.cimxml.graph; -import de.soptim.opencgmes.cimxml.CimVersion; -import org.apache.jena.mem2.GraphMem2Roaring; import org.apache.jena.riot.RDFParser; +import org.apache.jena.sparql.graph.GraphFactory; import org.junit.Test; import java.io.StringReader; -import java.util.Set; import static org.junit.Assert.*; @@ -56,7 +54,7 @@ public void parseProfileOntologyHeader() { """; - var graph = new GraphMem2Roaring(); + var graph = GraphFactory.createGraphMem(); RDFParser.create() .source(new StringReader(rdfxml)) @@ -67,12 +65,12 @@ public void parseProfileOntologyHeader() { var ontology = CimProfile.wrap(graph); assertFalse(ontology.isHeaderProfile()); - assertTrue(Set.of(CimVersion.CIM_17, CimVersion.CIM_18).contains(ontology.getCIMVersion())); + assertEquals(CimProfile17.CIM_NAMESPACE, ontology.getCimNamespace()); - assertEquals(2, ontology.getOwlVersionIRIs().size()); - assertTrue(ontology.getOwlVersionIRIs().stream() + assertEquals(2, ontology.getOwlVersionIris().size()); + assertTrue(ontology.getOwlVersionIris().stream() .anyMatch(n -> n.getURI().equals("http://example.org/MyCustom/Core/1/1"))); - assertTrue(ontology.getOwlVersionIRIs().stream() + assertTrue(ontology.getOwlVersionIris().stream() .anyMatch(n -> n.getURI().equals("http://example.org/MyCustom/Operation/1/1"))); assertEquals("1.1.0", ontology.getOwlVersionInfo()); assertEquals("MYCUST", ontology.getDcatKeyword()); @@ -95,7 +93,7 @@ public void parseProfileFileHeaderProfile() { """; - var graph = new GraphMem2Roaring(); + var graph = GraphFactory.createGraphMem(); RDFParser.create() .source(new StringReader(rdfxml)) @@ -106,7 +104,7 @@ public void parseProfileFileHeaderProfile() { var ontology = CimProfile.wrap(graph); assertTrue(ontology.isHeaderProfile()); - assertEquals(CimVersion.CIM_17, ontology.getCIMVersion()); + assertEquals(CimProfile17.CIM_NAMESPACE, ontology.getCimNamespace()); } } diff --git a/cimxml/src/test/java/de/soptim/opencgmes/cimxml/graph/TestCimProfile18.java b/cimxml/src/test/java/de/soptim/opencgmes/cimxml/graph/TestCimProfile18.java index a77ea7a0..2bf7c2e6 100644 --- a/cimxml/src/test/java/de/soptim/opencgmes/cimxml/graph/TestCimProfile18.java +++ b/cimxml/src/test/java/de/soptim/opencgmes/cimxml/graph/TestCimProfile18.java @@ -18,9 +18,8 @@ package de.soptim.opencgmes.cimxml.graph; -import de.soptim.opencgmes.cimxml.CimVersion; -import org.apache.jena.mem2.GraphMem2Roaring; import org.apache.jena.riot.RDFParser; +import org.apache.jena.sparql.graph.GraphFactory; import org.junit.Test; import java.io.StringReader; @@ -47,7 +46,7 @@ public void parseProfileFileHeaderProfile() { """; - var graph = new GraphMem2Roaring(); + var graph = GraphFactory.createGraphMem(); RDFParser.create() .source(new StringReader(rdfxml)) @@ -58,7 +57,7 @@ public void parseProfileFileHeaderProfile() { var ontology = CimProfile.wrap(graph); assertTrue(ontology.isHeaderProfile()); - assertEquals(CimVersion.CIM_18, ontology.getCIMVersion()); + assertEquals(CimProfile18.CIM_NAMESPACE, ontology.getCimNamespace()); } } diff --git a/cimxml/src/test/java/de/soptim/opencgmes/cimxml/rdfs/CimProfileRegistryStdTest.java b/cimxml/src/test/java/de/soptim/opencgmes/cimxml/rdfs/CimProfileRegistryStdTest.java index e0ebf5c4..f19dcc20 100644 --- a/cimxml/src/test/java/de/soptim/opencgmes/cimxml/rdfs/CimProfileRegistryStdTest.java +++ b/cimxml/src/test/java/de/soptim/opencgmes/cimxml/rdfs/CimProfileRegistryStdTest.java @@ -21,7 +21,7 @@ import de.soptim.opencgmes.cimxml.graph.CimProfile; import de.soptim.opencgmes.cimxml.parser.RdfXmlParser; import de.soptim.opencgmes.cimxml.parser.ReaderCIMXML_StAX_SR; -import de.soptim.opencgmes.cimxml.parser.system.StreamCIMXMLToDatasetGraph; +import de.soptim.opencgmes.cimxml.parser.system.StreamCimXmlToDatasetGraph; import org.apache.jena.datatypes.xsd.XSDDatatype; import org.apache.jena.graph.NodeFactory; import org.junit.Test; @@ -88,11 +88,11 @@ public void registerProfileWithOneClassAndTwoSimpleProperties() { """; final var parser = new ReaderCIMXML_StAX_SR(); - final var streamRDF = new StreamCIMXMLToDatasetGraph(); + final var streamRDF = new StreamCimXmlToDatasetGraph(); parser.read(new StringReader(rdfxml), streamRDF); - var graph = streamRDF.getCIMDatasetGraph().getDefaultGraph(); + var graph = streamRDF.getCimDatasetGraph().getDefaultGraph(); var profile = CimProfile.wrap(graph); @@ -183,7 +183,7 @@ public void registerPofileWithMultipleVersionIRIs() { NodeFactory.createURI("http://example.org/AnyOtherProfile/1/1")); assertFalse(registry.containsProfile(owlVersionIRIs)); - assertNull(registry.getPropertiesAndDatatypes(owlVersionIRIs)); + assert(registry.getPropertiesAndDatatypes(owlVersionIRIs).isEmpty()); } } @@ -375,7 +375,7 @@ public void registerProfilesWithSingleAndMultiVersionIrisMixed() { NodeFactory.createURI("http://example.org/AnyOtherProfile/1/1")); assertFalse(registry.containsProfile(owlVersionIRIs)); - assertNull(registry.getPropertiesAndDatatypes(owlVersionIRIs)); + assertTrue(registry.getPropertiesAndDatatypes(owlVersionIRIs).isEmpty()); } } diff --git a/cimxml/src/test/java/de/soptim/opencgmes/cimxml/sparql/core/CimDatasetGraphTest.java b/cimxml/src/test/java/de/soptim/opencgmes/cimxml/sparql/core/CimDatasetGraphTest.java index 151c085f..ef537f0c 100644 --- a/cimxml/src/test/java/de/soptim/opencgmes/cimxml/sparql/core/CimDatasetGraphTest.java +++ b/cimxml/src/test/java/de/soptim/opencgmes/cimxml/sparql/core/CimDatasetGraphTest.java @@ -20,6 +20,7 @@ import de.soptim.opencgmes.cimxml.parser.CimXmlParser; import org.apache.jena.graph.NodeFactory; +import org.apache.jena.riot.system.ErrorHandlerFactory; import org.junit.Test; import java.io.StringReader; @@ -42,7 +43,8 @@ public void fullModelToSingleGraph() { """; - var model = new CimXmlParser().parseCimModel(new StringReader(rdfxml)); + var model = new CimXmlParser(ErrorHandlerFactory.errorHandlerNoWarnings) + .parseCimModel(new StringReader(rdfxml)); var fullGraph = model.fullModelToSingleGraph(); assertNotNull(fullGraph); @@ -147,8 +149,10 @@ public void differenceModelToFullModel() { """; - var predecessorFullModel = new CimXmlParser().parseCimModel(new StringReader(rdfXmlFullModel)); - var differenceModel = new CimXmlParser().parseCimModel(new StringReader(rdfxmlDifferenceModel)); + var predecessorFullModel = new CimXmlParser(ErrorHandlerFactory.errorHandlerNoWarnings) + .parseCimModel(new StringReader(rdfXmlFullModel)); + var differenceModel = new CimXmlParser(ErrorHandlerFactory.errorHandlerNoWarnings) + .parseCimModel(new StringReader(rdfxmlDifferenceModel)); var fullGraph = differenceModel.differenceModelToFullModel(predecessorFullModel); assertNotNull(fullGraph); diff --git a/cimxml/src/test/java/de/soptim/opencgmes/parser/TestConvertCimXmlToJsonLd.java b/cimxml/src/test/java/de/soptim/opencgmes/parser/TestConvertCimXmlToJsonLd.java new file mode 100644 index 00000000..d39a52ca --- /dev/null +++ b/cimxml/src/test/java/de/soptim/opencgmes/parser/TestConvertCimXmlToJsonLd.java @@ -0,0 +1,138 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package de.soptim.opencgmes.parser; + +import de.soptim.opencgmes.cimxml.graph.CimProfile; +import de.soptim.opencgmes.cimxml.parser.CimXmlParser; +import de.soptim.opencgmes.cimxml.sparql.core.LinkedCimDatasetGraph; +import org.apache.jena.graph.NodeFactory; +import org.apache.jena.riot.Lang; +import org.apache.jena.riot.RDFDataMgr; +import org.junit.Ignore; +import org.junit.Test; + +import java.io.BufferedOutputStream; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; + +public class TestConvertCimXmlToJsonLd { + + private static final String CIM_SCHEMA_IRI = "cgmes:schema:"; + private static final String CIM_MODEL_IRI = "cgmes:model:"; + private static final String CIM_HEADER_IRI = "cgmes:header:"; + //private static final String CIM_SHACL_IRI = "cgmes:shacl:"; + //private static final String CIM_VALIDATION_REPORT_IRI = "cgmes:validationReport:"; + + public void convertCimXmlToJsonLd(String rdfsSchemaFolder, String cimXmlFolder, String jsonldOutputFile) { + convertCimXmlToJsonLd(Path.of(rdfsSchemaFolder), Path.of(cimXmlFolder), Path.of(jsonldOutputFile)); + } + + public void convertCimXmlToJsonLd(Path rdfsSchemaFolder, Path cimXmlFolder, Path jsonldOutputFile) { + final var dataset = new LinkedCimDatasetGraph(); + final var parser = new CimXmlParser(); + try (var rdfFiles = Files.list(rdfsSchemaFolder)) { + rdfFiles.parallel() + .filter(f -> f.toString().endsWith(".rdf")) + .forEach(rdfFile -> { + try { + final var schema = parser.parseAndRegisterCimProfile(rdfFile); + // add only header profiles to the dataset. + // the other profiles are only added when referenced in the dataset. + if(schema.isHeaderProfile()) { + dataset.addGraph(NodeFactory.createURI(CIM_SCHEMA_IRI + schema.getDcatKeyword()), schema); + dataset.prefixes().putAll(schema.getPrefixMapping()); + } + } catch (IOException e) { + throw new RuntimeException(e); + } + }); + } catch (IOException e) { + throw new RuntimeException(e); + } + + try (var cimXmlFiles = Files.list(cimXmlFolder)) { + cimXmlFiles.parallel() + .filter(f -> f.toString().endsWith(".xml")) + .forEach(cimXmlFile -> { + try { + final var cimDataset = parser.parseCimModel(cimXmlFile); + if(!cimDataset.isFullModel()) { + System.out.println("File " + cimXmlFile.getFileName() + " is not a FullModel. Skipping."); + return; + } + final var keywords = parser.getCimProfileRegistry() + .getMatchingProfiles(cimDataset.getModelHeader().getProfiles()) + .stream() + .peek(schema -> { + final var schemaGraphName = NodeFactory.createURI( + CIM_SCHEMA_IRI + schema.getDcatKeyword()); + if(!dataset.containsGraph(schemaGraphName)) { + dataset.addGraph(schemaGraphName, schema); + dataset.prefixes().putAll(schema.getPrefixMapping()); + } + }) + .map(CimProfile::getDcatKeyword) + .distinct() + .sorted() + .toList(); + if(keywords.isEmpty()) { + System.out.println("File " + cimXmlFile.getFileName() + " does not match any profile. Skipping."); + return; + } + var keyword = keywords.getFirst(); + if(keywords.size() > 1) { + System.out.println("File " + cimXmlFile.getFileName() + " matches multiple profiles: " + keywords + ". Using first one: " + keyword); + } + dataset.addGraph(NodeFactory.createURI(CIM_HEADER_IRI + keyword), cimDataset.getModelHeader()); + dataset.addGraph(NodeFactory.createURI(CIM_MODEL_IRI + keyword), cimDataset.getBody()); + dataset.prefixes().putAll(cimDataset.prefixes()); + } catch (IOException e) { + throw new RuntimeException(e); + } + }); + } catch (IOException e) { + throw new RuntimeException(e); + } + // create java nio file and output stream + try (var rdfFiles = new BufferedOutputStream(Files.newOutputStream(jsonldOutputFile))) { + RDFDataMgr.write(rdfFiles, dataset, Lang.JSONLD11); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + @Ignore + @Test + public void convertFullGridMerged() { + convertCimXmlToJsonLd( + "../data/ApplicationProfilesLibrary-release1-1-0/CGMES/RDFS/", + "../data/CGMES_ConformityAssessmentScheme_r3-0-2/CGMES_ConformityAssessmentScheme_TestConfigurations_v3-0-3/v3.0/FullGrid/FullGrid-Merged/", + "../output/FullGrid-Merged.jsonld"); + } + + @Ignore + @Test + public void convertRealGridMerged() { + convertCimXmlToJsonLd( + "../data/ApplicationProfilesLibrary-release1-1-0/CGMES/RDFS/", + "../data/CGMES_ConformityAssessmentScheme_r3-0-2/CGMES_ConformityAssessmentScheme_TestConfigurations_v3-0-3/v3.0/RealGrid/RealGrid-Merged/", + "../output/RealGrid-Merged.jsonld"); + } +} \ No newline at end of file diff --git a/cimxml/src/test/java/de/soptim/opencgmes/parser/TestParserCIMXMLConformity.java b/cimxml/src/test/java/de/soptim/opencgmes/parser/TestParserCIMXMLConformity.java index e5b5cc0b..559dcb2a 100644 --- a/cimxml/src/test/java/de/soptim/opencgmes/parser/TestParserCIMXMLConformity.java +++ b/cimxml/src/test/java/de/soptim/opencgmes/parser/TestParserCIMXMLConformity.java @@ -18,14 +18,16 @@ package de.soptim.opencgmes.parser; -import de.soptim.opencgmes.cimxml.CimVersion; import de.soptim.opencgmes.cimxml.graph.CimProfile; +import de.soptim.opencgmes.cimxml.graph.CimProfile17; +import de.soptim.opencgmes.cimxml.graph.CimProfile18; import de.soptim.opencgmes.cimxml.parser.ReaderCIMXML_StAX_SR; -import de.soptim.opencgmes.cimxml.parser.system.StreamCIMXMLToDatasetGraph; +import de.soptim.opencgmes.cimxml.parser.system.StreamCimXmlToDatasetGraph; import de.soptim.opencgmes.cimxml.rdfs.CimProfileRegistryStd; import org.apache.jena.datatypes.xsd.XSDDatatype; import org.apache.jena.graph.Node; import org.apache.jena.graph.NodeFactory; +import org.apache.jena.riot.system.ErrorHandlerFactory; import org.junit.Test; import java.io.StringReader; @@ -34,7 +36,6 @@ public class TestParserCIMXMLConformity { - /** * Test that the parser can parse a CIMXML document with a version declaration. * And that the version is correctly parsed. @@ -48,12 +49,12 @@ public void parseIEC61970_552Version() { """; - final var parser = new ReaderCIMXML_StAX_SR(); - final var streamRDF = new StreamCIMXMLToDatasetGraph(); + final var parser = new ReaderCIMXML_StAX_SR(ErrorHandlerFactory.errorHandlerNoWarnings); + final var streamRDF = new StreamCimXmlToDatasetGraph(); parser.read(new StringReader(rdfxml), streamRDF); - assertEquals("version=\"2.0\"", streamRDF.getVersionOfIEC61970_552()); + assertEquals("version=\"2.0\"", streamRDF.getVersionOfIec61970_552()); } /** @@ -67,16 +68,16 @@ public void parseWithoutIEC61970_552Version() { """; - final var parser = new ReaderCIMXML_StAX_SR(); - final var streamRDF = new StreamCIMXMLToDatasetGraph(); + final var parser = new ReaderCIMXML_StAX_SR(ErrorHandlerFactory.errorHandlerNoWarnings); + final var streamRDF = new StreamCimXmlToDatasetGraph(); parser.read(new StringReader(rdfxml), streamRDF); - assertNull(streamRDF.getVersionOfIEC61970_552()); + assertNull(streamRDF.getVersionOfIec61970_552()); } @Test - public void parseCIMVersion17() { + public void parseCimNamespace17() { final var rdfxml = """ """; - final var parser = new ReaderCIMXML_StAX_SR(); - final var streamRDF = new StreamCIMXMLToDatasetGraph(); + final var parser = new ReaderCIMXML_StAX_SR(ErrorHandlerFactory.errorHandlerNoWarnings); + final var streamRDF = new StreamCimXmlToDatasetGraph(); parser.read(new StringReader(rdfxml), streamRDF); - assertEquals(CimVersion.CIM_17, streamRDF.getVersionOfCIMXML()); + assertEquals(CimProfile17.CIM_NAMESPACE, streamRDF.getCimNamespace()); } @Test - public void parseCIMVersion18() { + public void parseCimNamespace18() { final var rdfxml = """ """; - final var parser = new ReaderCIMXML_StAX_SR(); - final var streamRDF = new StreamCIMXMLToDatasetGraph(); + final var parser = new ReaderCIMXML_StAX_SR(ErrorHandlerFactory.errorHandlerNoWarnings); + final var streamRDF = new StreamCimXmlToDatasetGraph(); parser.read(new StringReader(rdfxml), streamRDF); - assertEquals(CimVersion.CIM_18, streamRDF.getVersionOfCIMXML()); + assertEquals(CimProfile18.CIM_NAMESPACE, streamRDF.getCimNamespace()); } @Test @@ -125,20 +126,20 @@ public void parseFullModelHeaderAndContentInDifferentGraphs() { """; - final var parser = new ReaderCIMXML_StAX_SR(); - final var streamRDF = new StreamCIMXMLToDatasetGraph(); + final var parser = new ReaderCIMXML_StAX_SR(ErrorHandlerFactory.errorHandlerNoWarnings); + final var streamRDF = new StreamCimXmlToDatasetGraph(); parser.read(new StringReader(rdfxml), streamRDF); - assertTrue(streamRDF.getCIMDatasetGraph().isFullModel()); + assertTrue(streamRDF.getCimDatasetGraph().isFullModel()); - assertNotNull(streamRDF.getCIMDatasetGraph().getModelHeader()); - var modelHeader = streamRDF.getCIMDatasetGraph().getModelHeader(); + assertNotNull(streamRDF.getCimDatasetGraph().getModelHeader()); + var modelHeader = streamRDF.getCimDatasetGraph().getModelHeader(); assertEquals("urn:uuid:08984e27-811f-4042-9125-1531ae0de0f6", modelHeader.getModel().toString()); assertEquals(1, modelHeader.getProfiles().size()); assertTrue(modelHeader.getProfiles().stream().map(Node::getLiteralLexicalForm).toList() .contains("http://iec.ch/TC57/ns/CIM/CoreEquipment-EU/3.0")); - var graph = streamRDF.getCIMDatasetGraph().getDefaultGraph(); + var graph = streamRDF.getCimDatasetGraph().getDefaultGraph(); assertTrue(graph.contains( NodeFactory.createURI("urn:uuid:f67fc354-9e39-4191-a456-67537399bc48"), NodeFactory.createURI("http://www.w3.org/1999/02/22-rdf-syntax-ns#type"), @@ -206,18 +207,18 @@ public void parseDifferenceModelHeaderAndContentInDifferentGraphs() { """; - final var parser = new ReaderCIMXML_StAX_SR(); - final var streamRDF = new StreamCIMXMLToDatasetGraph(); + final var parser = new ReaderCIMXML_StAX_SR(ErrorHandlerFactory.errorHandlerNoWarnings); + final var streamRDF = new StreamCimXmlToDatasetGraph(); parser.read(new StringReader(rdfxml), streamRDF); - assertTrue(streamRDF.getCIMDatasetGraph().isDifferenceModel()); - assertNotNull(streamRDF.getCIMDatasetGraph().getModelHeader()); - var modelHeader = streamRDF.getCIMDatasetGraph().getModelHeader(); + assertTrue(streamRDF.getCimDatasetGraph().isDifferenceModel()); + assertNotNull(streamRDF.getCimDatasetGraph().getModelHeader()); + var modelHeader = streamRDF.getCimDatasetGraph().getModelHeader(); assertEquals("urn:uuid:08984e27-811f-4042-9125-1531ae0de0f6", modelHeader.getModel().toString()); - var preconditions = streamRDF.getCIMDatasetGraph().getPreconditions(); + var preconditions = streamRDF.getCimDatasetGraph().getPreconditions(); assertNotNull(preconditions); assertEquals(1, preconditions.size()); assertTrue(preconditions.contains( @@ -226,7 +227,7 @@ public void parseDifferenceModelHeaderAndContentInDifferentGraphs() { NodeFactory.createLiteralString("Name of my element") )); - var forwardDifferences = streamRDF.getCIMDatasetGraph().getForwardDifferences(); + var forwardDifferences = streamRDF.getCimDatasetGraph().getForwardDifferences(); assertNotNull(forwardDifferences); assertEquals(4, forwardDifferences.size()); assertTrue(forwardDifferences.contains( @@ -250,7 +251,7 @@ public void parseDifferenceModelHeaderAndContentInDifferentGraphs() { NodeFactory.createLiteralString("property of new element") )); - var reverseDifferences = streamRDF.getCIMDatasetGraph().getReverseDifferences(); + var reverseDifferences = streamRDF.getCimDatasetGraph().getReverseDifferences(); assertNotNull(reverseDifferences); assertEquals(4, reverseDifferences.size()); assertTrue(reverseDifferences.contains( @@ -299,29 +300,29 @@ public void writePrefixesToAllGraphInDifferenceModel() { """; - final var parser = new ReaderCIMXML_StAX_SR(); - final var streamRDF = new StreamCIMXMLToDatasetGraph(); + final var parser = new ReaderCIMXML_StAX_SR(ErrorHandlerFactory.errorHandlerNoWarnings); + final var streamRDF = new StreamCimXmlToDatasetGraph(); parser.read(new StringReader(rdfxml), streamRDF); - assertTrue(streamRDF.getCIMDatasetGraph().isDifferenceModel()); - assertNotNull(streamRDF.getCIMDatasetGraph().getModelHeader()); - var modelHeader = streamRDF.getCIMDatasetGraph().getModelHeader(); + assertTrue(streamRDF.getCimDatasetGraph().isDifferenceModel()); + assertNotNull(streamRDF.getCimDatasetGraph().getModelHeader()); + var modelHeader = streamRDF.getCimDatasetGraph().getModelHeader(); assertEquals(4, modelHeader.getPrefixMapping().numPrefixes()); assertEquals("urn:uuid:08984e27-811f-4042-9125-1531ae0de0f6", modelHeader.getModel().toString()); - var preconditions = streamRDF.getCIMDatasetGraph().getPreconditions(); + var preconditions = streamRDF.getCimDatasetGraph().getPreconditions(); assertNotNull(preconditions); assertTrue(preconditions.getPrefixMapping().samePrefixMappingAs(modelHeader.getPrefixMapping())); assertEquals(0, preconditions.size()); - var forwardDifferences = streamRDF.getCIMDatasetGraph().getForwardDifferences(); + var forwardDifferences = streamRDF.getCimDatasetGraph().getForwardDifferences(); assertNotNull(forwardDifferences); assertTrue(forwardDifferences.getPrefixMapping().samePrefixMappingAs(modelHeader.getPrefixMapping())); assertEquals(0, forwardDifferences.size()); - var reverseDifferences = streamRDF.getCIMDatasetGraph().getReverseDifferences(); + var reverseDifferences = streamRDF.getCimDatasetGraph().getReverseDifferences(); assertNotNull(reverseDifferences); assertTrue(reverseDifferences.getPrefixMapping().samePrefixMappingAs(modelHeader.getPrefixMapping())); assertEquals(0, reverseDifferences.size()); @@ -342,11 +343,11 @@ public void replaceUnderscoresInRdfAboutAndRdfId() { """; final var parser = new ReaderCIMXML_StAX_SR(); - final var streamRDF = new StreamCIMXMLToDatasetGraph(); + final var streamRDF = new StreamCimXmlToDatasetGraph(); parser.read(new StringReader(rdfxml), streamRDF); - var graph = streamRDF.getCIMDatasetGraph().getDefaultGraph(); + var graph = streamRDF.getCimDatasetGraph().getDefaultGraph(); assertTrue(graph.contains( NodeFactory.createURI("urn:uuid:f67fc354-9e39-4191-a456-67537399bc48"), NodeFactory.createURI("http://www.w3.org/1999/02/22-rdf-syntax-ns#type"), @@ -378,12 +379,12 @@ public void replaceUnderscoresInRdfAboutAndRdfIdFixingMissingDashesInUuids() { """; - final var parser = new ReaderCIMXML_StAX_SR(); - final var streamRDF = new StreamCIMXMLToDatasetGraph(); + final var parser = new ReaderCIMXML_StAX_SR(ErrorHandlerFactory.errorHandlerNoWarnings); + final var streamRDF = new StreamCimXmlToDatasetGraph(); parser.read(new StringReader(rdfxml), streamRDF); - var graph = streamRDF.getCIMDatasetGraph().getDefaultGraph(); + var graph = streamRDF.getCimDatasetGraph().getDefaultGraph(); assertTrue(graph.contains( NodeFactory.createURI("urn:uuid:f67fc354-9e39-4191-a456-67537399bc48"), NodeFactory.createURI("http://www.w3.org/1999/02/22-rdf-syntax-ns#type"), @@ -507,14 +508,14 @@ public void fullModelWithProfilesAndDatatypes() { final var parser = new ReaderCIMXML_StAX_SR(); - final var streamFileHeaderHeaderProfile = new StreamCIMXMLToDatasetGraph(); + final var streamFileHeaderHeaderProfile = new StreamCimXmlToDatasetGraph(); parser.read(new StringReader(rdfxmlFileHeaderProfile), streamFileHeaderHeaderProfile); - var fileHeaderGraph = streamFileHeaderHeaderProfile.getCIMDatasetGraph().getDefaultGraph(); + var fileHeaderGraph = streamFileHeaderHeaderProfile.getCimDatasetGraph().getDefaultGraph(); var fileHeaderProfile = CimProfile.wrap(fileHeaderGraph); - final var streamRDFProfile = new StreamCIMXMLToDatasetGraph(); + final var streamRDFProfile = new StreamCimXmlToDatasetGraph(); parser.read(new StringReader(rdfxmlCimProfile), streamRDFProfile); - var profileGraph = streamRDFProfile.getCIMDatasetGraph().getDefaultGraph(); + var profileGraph = streamRDFProfile.getCimDatasetGraph().getDefaultGraph(); var profile = CimProfile.wrap(profileGraph); @@ -523,10 +524,10 @@ public void fullModelWithProfilesAndDatatypes() { registry.register(profile); - final var streamInstanceData = new StreamCIMXMLToDatasetGraph(); + final var streamInstanceData = new StreamCimXmlToDatasetGraph(); parser.read(new StringReader(cimxmlInstanceData), registry, streamInstanceData); - var instanceGraph = streamInstanceData.getCIMDatasetGraph().getBody(); + var instanceGraph = streamInstanceData.getCimDatasetGraph().getBody(); assertNotNull(instanceGraph); assertEquals(6, instanceGraph.size()); diff --git a/cimxml/src/test/java/de/soptim/opencgmes/parser/TestParserRDFXMLConformity.java b/cimxml/src/test/java/de/soptim/opencgmes/parser/TestParserRDFXMLConformity.java index 80711d1e..d2960fc3 100644 --- a/cimxml/src/test/java/de/soptim/opencgmes/parser/TestParserRDFXMLConformity.java +++ b/cimxml/src/test/java/de/soptim/opencgmes/parser/TestParserRDFXMLConformity.java @@ -19,13 +19,12 @@ package de.soptim.opencgmes.parser; import de.soptim.opencgmes.cimxml.parser.ReaderCIMXML_StAX_SR; -import de.soptim.opencgmes.cimxml.parser.system.StreamCIMXMLToDatasetGraph; +import de.soptim.opencgmes.cimxml.parser.system.StreamCimXmlToDatasetGraph; import org.apache.jena.graph.Graph; -import org.apache.jena.mem2.GraphMem2Roaring; -import org.apache.jena.mem2.IndexingStrategy; import org.apache.jena.riot.RDFParser; import org.apache.jena.riot.system.PrefixMap; import org.apache.jena.shared.PrefixMapping; +import org.apache.jena.sparql.graph.GraphFactory; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; @@ -149,14 +148,14 @@ public void testW3cRdfXmlExample() throws Exception { public void parseAndCompare(Path rdfxml, Path nTriples) throws Exception { Objects.requireNonNull(rdfxml); - final var expectedGraph = new GraphMem2Roaring(IndexingStrategy.LAZY); + final var expectedGraph = GraphFactory.createGraphMem(); final var parser = new ReaderCIMXML_StAX_SR(); - final var streamRDF = new StreamCIMXMLToDatasetGraph(); + final var streamRDF = new StreamCimXmlToDatasetGraph(); try(var fileReader = new FileReader(rdfxml.toFile())) { parser.read(fileReader, streamRDF); } - var graph = streamRDF.getCIMDatasetGraph().getDefaultGraph(); + var graph = streamRDF.getCimDatasetGraph().getDefaultGraph(); RDFParser.create() .source(rdfxml) @@ -165,10 +164,10 @@ public void parseAndCompare(Path rdfxml, Path nTriples) throws Exception { .parse(expectedGraph); assertGraphEquals(expectedGraph, graph); - assertPrefixMappingEquals(expectedGraph.getPrefixMapping(), streamRDF.getCIMDatasetGraph().prefixes()); + assertPrefixMappingEquals(expectedGraph.getPrefixMapping(), streamRDF.getCimDatasetGraph().prefixes()); if (nTriples != null) { - final var nTriplesGraph = new GraphMem2Roaring(IndexingStrategy.LAZY); + final var nTriplesGraph = GraphFactory.createGraphMem(); RDFParser.create() .source(nTriples) .lang(org.apache.jena.riot.Lang.NTRIPLES) From 705189ded24d28aee4b74cd2fe38b83be713feed Mon Sep 17 00:00:00 2001 From: bern-SOPTIM Date: Sat, 17 Jan 2026 20:06:06 +0100 Subject: [PATCH 2/4] GH-2: Support user defined non-XSD data types - for properties with rdf:range, CimProfileRegistry now checks the Jena TypeMapper for registered RDFDataTypes before treating the rdf:range value as reference - added test for this behaviour - added test for supporting custom datatypes - Refactored SPARQL properties query - updated formatting of tests to new style conventions - CimProfileRegistry now uses ConcurrentHashMap for primitive type mapping --- .../cimxml/rdfs/CimProfileRegistryStd.java | 77 ++-- .../rdfs/CimProfileRegistryStdTest.java | 382 ----------------- .../rdfs/TestCimProfileRegistryStd.java | 384 ++++++++++++++++++ .../TestCimProfileRegistryStdTypeMapping.java | 203 +++++++++ .../sparql/core/CimDatasetGraphTest.java | 230 ----------- .../sparql/core/TestCimDatasetGraph.java | 232 +++++++++++ .../parser/TestConvertCimXmlToJsonLd.java | 209 +++++----- 7 files changed, 969 insertions(+), 748 deletions(-) delete mode 100644 cimxml/src/test/java/de/soptim/opencgmes/cimxml/rdfs/CimProfileRegistryStdTest.java create mode 100644 cimxml/src/test/java/de/soptim/opencgmes/cimxml/rdfs/TestCimProfileRegistryStd.java create mode 100644 cimxml/src/test/java/de/soptim/opencgmes/cimxml/rdfs/TestCimProfileRegistryStdTypeMapping.java delete mode 100644 cimxml/src/test/java/de/soptim/opencgmes/cimxml/sparql/core/CimDatasetGraphTest.java create mode 100644 cimxml/src/test/java/de/soptim/opencgmes/cimxml/sparql/core/TestCimDatasetGraph.java diff --git a/cimxml/src/main/java/de/soptim/opencgmes/cimxml/rdfs/CimProfileRegistryStd.java b/cimxml/src/main/java/de/soptim/opencgmes/cimxml/rdfs/CimProfileRegistryStd.java index 97c65933..d1fc8290 100644 --- a/cimxml/src/main/java/de/soptim/opencgmes/cimxml/rdfs/CimProfileRegistryStd.java +++ b/cimxml/src/main/java/de/soptim/opencgmes/cimxml/rdfs/CimProfileRegistryStd.java @@ -27,6 +27,7 @@ import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import org.apache.jena.datatypes.RDFDatatype; +import org.apache.jena.datatypes.TypeMapper; import org.apache.jena.datatypes.xsd.XSDDatatype; import org.apache.jena.datatypes.xsd.impl.RDFLangString; import org.apache.jena.graph.Graph; @@ -46,40 +47,37 @@ public class CimProfileRegistryStd implements CimProfileRegistry { private static final Query typedPropertiesQuery = QueryFactory.create( """ - PREFIX rdf: - PREFIX rdfs: - PREFIX cims: - - SELECT ?rdfType ?property ?cimDatatype ?primitiveType ?referenceType - WHERE - { - { - ?property rdfs:domain ?rdfType; - rdfs:range ?referenceType; - OPTIONAL { - ?property cims:AssociationUsed ?associationUsed - } - FILTER(!BOUND(?associationUsed) || ?associationUsed = "Yes") - } - UNION - { - ?property rdfs:domain ?rdfType; - cims:dataType ?cimDatatype. - { - ?cimDatatype cims:stereotype "CIMDatatype". - [] rdfs:domain ?cimDatatype; - rdfs:label ?label; - #rdfs:label "value"; - cims:dataType/cims:stereotype "Primitive"; - cims:dataType/rdfs:label ?primitiveType. - FILTER (!bound(?label) || str(?label) = "value") - } - UNION + PREFIX cims: + PREFIX rdf: + PREFIX rdfs: + PREFIX xsd: + + SELECT ?rdfType ?property ?cimDatatype ?primitiveType ?referenceType + WHERE { - ?cimDatatype cims:stereotype "Primitive"; - rdfs:label ?primitiveType. + ?rdfType rdf:type rdfs:Class . + ?property rdf:type rdf:Property ; + rdfs:domain ?rdfType . + { + ?property rdfs:range ?referenceType. + FILTER NOT EXISTS { ?property cims:AssociationUsed "No" } # Filter out associations that are not used as properties + } + UNION + { + ?property cims:dataType ?cimDatatype. + { + ?cimDatatype cims:stereotype "Primitive"; + rdfs:label ?primitiveType. + } + UNION + { + ?cimDatatype cims:stereotype "CIMDatatype". + BIND(IRI(CONCAT(STR(?cimDatatype), ".value")) AS ?parentTypeValue) + ?parentTypeValue rdfs:domain ?cimDatatype; + cims:dataType/cims:stereotype "Primitive"; + cims:dataType/rdfs:label ?primitiveType. + } } - } } """); private final ErrorHandler errorHandler; @@ -323,14 +321,21 @@ private Map getTypedProperties(Graph g) { final var cimDatatype = vars.get("cimDatatype"); final var primitiveType = vars.get("primitiveType"); final var referenceType = vars.get("referenceType"); + final var rdfDataType = primitiveType != null + // retrieve CIM specific primitive type mapping + ? getXsdDatatype(primitiveType.getLiteralLexicalForm()) + // if no cimDatatype is given, but a referenceType is given + : cimDatatype == null && referenceType != null + // then this may be an XSD datatype defined as rdf:range + // if it cannot be mapped, we treat it as a reference type + ? TypeMapper.getInstance().getTypeByName(referenceType.getURI()) + : null; map.put(property, new PropertyInfo( rdfType, property, cimDatatype, - primitiveType != null - ? getXsdDatatype(primitiveType.getLiteralLexicalForm()) - : null, - referenceType)); + rdfDataType, + rdfDataType != null ? null : referenceType)); }); return Collections.unmodifiableMap(map); } diff --git a/cimxml/src/test/java/de/soptim/opencgmes/cimxml/rdfs/CimProfileRegistryStdTest.java b/cimxml/src/test/java/de/soptim/opencgmes/cimxml/rdfs/CimProfileRegistryStdTest.java deleted file mode 100644 index f19dcc20..00000000 --- a/cimxml/src/test/java/de/soptim/opencgmes/cimxml/rdfs/CimProfileRegistryStdTest.java +++ /dev/null @@ -1,382 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package de.soptim.opencgmes.cimxml.rdfs; - -import de.soptim.opencgmes.cimxml.graph.CimProfile; -import de.soptim.opencgmes.cimxml.parser.RdfXmlParser; -import de.soptim.opencgmes.cimxml.parser.ReaderCIMXML_StAX_SR; -import de.soptim.opencgmes.cimxml.parser.system.StreamCimXmlToDatasetGraph; -import org.apache.jena.datatypes.xsd.XSDDatatype; -import org.apache.jena.graph.NodeFactory; -import org.junit.Test; - -import java.io.StringReader; -import java.util.Set; - -import static org.junit.Assert.*; - -public class CimProfileRegistryStdTest { - - @Test - public void registerProfileWithOneClassAndTwoSimpleProperties() { - final var rdfxml = """ - - - - - MYCUST - - 1.1.0 - - - - - - - - - - - - - - - - - - - - - - - - - Float - Primitive - - - - - String - Primitive - - - - """; - - final var parser = new ReaderCIMXML_StAX_SR(); - final var streamRDF = new StreamCimXmlToDatasetGraph(); - - parser.read(new StringReader(rdfxml), streamRDF); - - var graph = streamRDF.getCimDatasetGraph().getDefaultGraph(); - - var profile = CimProfile.wrap(graph); - - var registry = new CimProfileRegistryStd(); - registry.register(profile); - - var owlVersionIRIs = Set.of(NodeFactory.createURI("http://example.org/MyCustom/1/1")); - - assertTrue(registry.containsProfile(owlVersionIRIs)); - - assertTrue(registry.getRegisteredProfiles().contains(profile)); - assertEquals(1, registry.getRegisteredProfiles().size()); - - var properties = registry.getPropertiesAndDatatypes(owlVersionIRIs); - assertNotNull(properties); - assertEquals(2, properties.size()); - - var floatProperty = NodeFactory.createURI("http://iec.ch/TC57/CIM100#ClassA.floatProperty"); - assertTrue(properties.containsKey(floatProperty)); - var propertyInfo = properties.get(floatProperty); - assertEquals(NodeFactory.createURI("http://iec.ch/TC57/CIM100#ClassA"), propertyInfo.rdfType()); - assertEquals(floatProperty, propertyInfo.property()); - assertEquals(XSDDatatype.XSDfloat, propertyInfo.primitiveType()); - assertNull(propertyInfo.referenceType()); - - var textProperty = NodeFactory.createURI("http://iec.ch/TC57/CIM100#ClassA.textProperty"); - assertTrue(properties.containsKey(textProperty)); - propertyInfo = properties.get(textProperty); - assertEquals(NodeFactory.createURI("http://iec.ch/TC57/CIM100#ClassA"), propertyInfo.rdfType()); - assertEquals(textProperty, propertyInfo.property()); - assertEquals(XSDDatatype.XSDstring, propertyInfo.primitiveType()); - assertNull(propertyInfo.referenceType()); - } - - @Test - public void registerPofileWithMultipleVersionIRIs() { - final var rdfxml = """ - - - - - MYCUST - - - - - - """; - - - var profile = new RdfXmlParser().parseCimProfile(new StringReader(rdfxml)); - - var registry = new CimProfileRegistryStd(); - registry.register(profile); - - { - var owlVersionIRIs = Set.of( - NodeFactory.createURI("http://example.org/MyCustomCore/1/1"), - NodeFactory.createURI("http://example.org/MyCustomOperation/1/1")); - - assertTrue(registry.containsProfile(owlVersionIRIs)); - assertNotNull(registry.getPropertiesAndDatatypes(owlVersionIRIs)); - } - - { - var owlVersionIRIs = Set.of( - NodeFactory.createURI("http://example.org/MyCustomCore/1/1")); - - assertTrue(registry.containsProfile(owlVersionIRIs)); - assertNotNull(registry.getPropertiesAndDatatypes(owlVersionIRIs)); - } - - { - var owlVersionIRIs = Set.of( - NodeFactory.createURI("http://example.org/MyCustomOperation/1/1")); - - assertTrue(registry.containsProfile(owlVersionIRIs)); - assertNotNull(registry.getPropertiesAndDatatypes(owlVersionIRIs)); - } - - { - var owlVersionIRIs = Set.of( - NodeFactory.createURI("http://example.org/AnyOtherProfile/1/1")); - - assertFalse(registry.containsProfile(owlVersionIRIs)); - assert(registry.getPropertiesAndDatatypes(owlVersionIRIs).isEmpty()); - } - } - - @Test - public void registerProfilesWithSameVersionIris() { - final var rdfxml = """ - - - - - MYCUST - - - - - """; - - - var profileA = new RdfXmlParser().parseCimProfile(new StringReader(rdfxml)); - var profileB = new RdfXmlParser().parseCimProfile(new StringReader(rdfxml)); - - - var registry = new CimProfileRegistryStd(); - registry.register(profileA); - - assertThrows(IllegalArgumentException.class, () -> registry.register(profileB)); - } - - @Test - public void registerProfilesWithMultipleSameVersionIris() { - final var rdfxml = """ - - - - - MYCUST - - - - - - """; - - var profileA = new RdfXmlParser().parseCimProfile(new StringReader(rdfxml)); - var profileB = new RdfXmlParser().parseCimProfile(new StringReader(rdfxml)); - - - var registry = new CimProfileRegistryStd(); - registry.register(profileA); - - assertThrows(IllegalArgumentException.class, () -> registry.register(profileB)); - } - - @Test - public void registerProfilesWithSingleAndMultiVersionIrisMixed() { - final var rdfxmlProfileA = """ - - - - - MYCUST - - - - - - - - - - - - - - - - - - - - Float - Primitive - - - - """; - - final var rdfxmlProfileB = """ - - - - - MYCUST - - - - - - - - - - - - - - - - - - - Float - Primitive - - - - """; - - var profileA = new RdfXmlParser().parseCimProfile(new StringReader(rdfxmlProfileA)); - var profileB = new RdfXmlParser().parseCimProfile(new StringReader(rdfxmlProfileB)); - - - var registry = new CimProfileRegistryStd(); - registry.register(profileA); - registry.register(profileB); - - //add in different order - registry = new CimProfileRegistryStd(); - registry.register(profileB); - registry.register(profileA); - - { - var owlVersionIRIs = Set.of( - NodeFactory.createURI("http://example.org/MyCustomCore/1/1"), - NodeFactory.createURI("http://example.org/MyCustomOperation/1/1")); - - assertTrue(registry.containsProfile(owlVersionIRIs)); - var properties = registry.getPropertiesAndDatatypes(owlVersionIRIs); - assertNotNull(properties); - assertTrue(properties.containsKey(NodeFactory.createURI("http://iec.ch/TC57/CIM100#ClassA.floatProperty"))); - } - - { - var owlVersionIRIs = Set.of( - NodeFactory.createURI("http://example.org/MyCustomCore/1/1")); - - assertTrue(registry.containsProfile(owlVersionIRIs)); - var properties = registry.getPropertiesAndDatatypes(owlVersionIRIs); - assertNotNull(properties); - assertTrue(properties.containsKey(NodeFactory.createURI("http://iec.ch/TC57/CIM100#ClassB.floatProperty"))); - } - - { - var owlVersionIRIs = Set.of( - NodeFactory.createURI("http://example.org/MyCustomOperation/1/1")); - - assertTrue(registry.containsProfile(owlVersionIRIs)); - var properties = registry.getPropertiesAndDatatypes(owlVersionIRIs); - assertNotNull(properties); - assertTrue(properties.containsKey(NodeFactory.createURI("http://iec.ch/TC57/CIM100#ClassA.floatProperty"))); - } - - { - var owlVersionIRIs = Set.of( - NodeFactory.createURI("http://example.org/AnyOtherProfile/1/1")); - - assertFalse(registry.containsProfile(owlVersionIRIs)); - assertTrue(registry.getPropertiesAndDatatypes(owlVersionIRIs).isEmpty()); - } - } - -} \ No newline at end of file diff --git a/cimxml/src/test/java/de/soptim/opencgmes/cimxml/rdfs/TestCimProfileRegistryStd.java b/cimxml/src/test/java/de/soptim/opencgmes/cimxml/rdfs/TestCimProfileRegistryStd.java new file mode 100644 index 00000000..7ee01cca --- /dev/null +++ b/cimxml/src/test/java/de/soptim/opencgmes/cimxml/rdfs/TestCimProfileRegistryStd.java @@ -0,0 +1,384 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package de.soptim.opencgmes.cimxml.rdfs; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; + +import de.soptim.opencgmes.cimxml.graph.CimProfile; +import de.soptim.opencgmes.cimxml.parser.RdfXmlParser; +import de.soptim.opencgmes.cimxml.parser.ReaderCIMXML_StAX_SR; +import de.soptim.opencgmes.cimxml.parser.system.StreamCimXmlToDatasetGraph; +import java.io.StringReader; +import java.util.Set; +import org.apache.jena.datatypes.xsd.XSDDatatype; +import org.apache.jena.graph.NodeFactory; +import org.junit.Test; + +public class TestCimProfileRegistryStd { + + @Test + public void registerProfileWithOneClassAndTwoSimpleProperties() { + final var rdfxml = """ + + + + + MYCUST + + 1.1.0 + + + + + + + + + + + + + + + + + + + + + + + + + Float + Primitive + + + + + String + Primitive + + + + """; + + final var parser = new ReaderCIMXML_StAX_SR(); + final var streamRDF = new StreamCimXmlToDatasetGraph(); + + parser.read(new StringReader(rdfxml), streamRDF); + + var graph = streamRDF.getCimDatasetGraph().getDefaultGraph(); + + var profile = CimProfile.wrap(graph); + + var registry = new CimProfileRegistryStd(); + registry.register(profile); + + var owlVersionIRIs = Set.of(NodeFactory.createURI("http://example.org/MyCustom/1/1")); + + assertTrue(registry.containsProfile(owlVersionIRIs)); + + assertTrue(registry.getRegisteredProfiles().contains(profile)); + assertEquals(1, registry.getRegisteredProfiles().size()); + + var properties = registry.getPropertiesAndDatatypes(owlVersionIRIs); + assertNotNull(properties); + assertEquals(2, properties.size()); + + var floatProperty = NodeFactory.createURI("http://iec.ch/TC57/CIM100#ClassA.floatProperty"); + assertTrue(properties.containsKey(floatProperty)); + var propertyInfo = properties.get(floatProperty); + assertEquals(NodeFactory.createURI("http://iec.ch/TC57/CIM100#ClassA"), propertyInfo.rdfType()); + assertEquals(floatProperty, propertyInfo.property()); + assertEquals(XSDDatatype.XSDfloat, propertyInfo.primitiveType()); + assertNull(propertyInfo.referenceType()); + + var textProperty = NodeFactory.createURI("http://iec.ch/TC57/CIM100#ClassA.textProperty"); + assertTrue(properties.containsKey(textProperty)); + propertyInfo = properties.get(textProperty); + assertEquals(NodeFactory.createURI("http://iec.ch/TC57/CIM100#ClassA"), propertyInfo.rdfType()); + assertEquals(textProperty, propertyInfo.property()); + assertEquals(XSDDatatype.XSDstring, propertyInfo.primitiveType()); + assertNull(propertyInfo.referenceType()); + } + + @Test + public void registerProfileWithMultipleVersionIRIs() { + final var rdfxml = """ + + + + + MYCUST + + + + + + """; + + var profile = new RdfXmlParser().parseCimProfile(new StringReader(rdfxml)); + + var registry = new CimProfileRegistryStd(); + registry.register(profile); + + { + var owlVersionIRIs = Set.of( + NodeFactory.createURI("http://example.org/MyCustomCore/1/1"), + NodeFactory.createURI("http://example.org/MyCustomOperation/1/1")); + + assertTrue(registry.containsProfile(owlVersionIRIs)); + assertNotNull(registry.getPropertiesAndDatatypes(owlVersionIRIs)); + } + + { + var owlVersionIRIs = Set.of( + NodeFactory.createURI("http://example.org/MyCustomCore/1/1")); + + assertTrue(registry.containsProfile(owlVersionIRIs)); + assertNotNull(registry.getPropertiesAndDatatypes(owlVersionIRIs)); + } + + { + var owlVersionIRIs = Set.of( + NodeFactory.createURI("http://example.org/MyCustomOperation/1/1")); + + assertTrue(registry.containsProfile(owlVersionIRIs)); + assertNotNull(registry.getPropertiesAndDatatypes(owlVersionIRIs)); + } + + { + var owlVersionIRIs = Set.of( + NodeFactory.createURI("http://example.org/AnyOtherProfile/1/1")); + + assertFalse(registry.containsProfile(owlVersionIRIs)); + assert (registry.getPropertiesAndDatatypes(owlVersionIRIs).isEmpty()); + } + } + + @Test + public void registerProfilesWithSameVersionIris() { + final var rdfxml = """ + + + + + MYCUST + + + + + """; + + var profileA = new RdfXmlParser().parseCimProfile(new StringReader(rdfxml)); + var profileB = new RdfXmlParser().parseCimProfile(new StringReader(rdfxml)); + + var registry = new CimProfileRegistryStd(); + registry.register(profileA); + + assertThrows(IllegalArgumentException.class, () -> registry.register(profileB)); + } + + @Test + public void registerProfilesWithMultipleSameVersionIris() { + final var rdfxml = """ + + + + + MYCUST + + + + + + """; + + var profileA = new RdfXmlParser().parseCimProfile(new StringReader(rdfxml)); + var profileB = new RdfXmlParser().parseCimProfile(new StringReader(rdfxml)); + + var registry = new CimProfileRegistryStd(); + registry.register(profileA); + + assertThrows(IllegalArgumentException.class, () -> registry.register(profileB)); + } + + @Test + public void registerProfilesWithSingleAndMultiVersionIrisMixed() { + final var rdfxmlProfileA = """ + + + + + MYCUST + + + + + + + + + + + + + + + + + + + + Float + Primitive + + + + """; + + final var rdfxmlProfileB = """ + + + + + MYCUST + + + + + + + + + + + + + + + + + + + Float + Primitive + + + + """; + + var profileA = new RdfXmlParser().parseCimProfile(new StringReader(rdfxmlProfileA)); + var profileB = new RdfXmlParser().parseCimProfile(new StringReader(rdfxmlProfileB)); + + var registry = new CimProfileRegistryStd(); + registry.register(profileA); + registry.register(profileB); + + //add in different order + registry = new CimProfileRegistryStd(); + registry.register(profileB); + registry.register(profileA); + + { + var owlVersionIRIs = Set.of( + NodeFactory.createURI("http://example.org/MyCustomCore/1/1"), + NodeFactory.createURI("http://example.org/MyCustomOperation/1/1")); + + assertTrue(registry.containsProfile(owlVersionIRIs)); + var properties = registry.getPropertiesAndDatatypes(owlVersionIRIs); + assertNotNull(properties); + assertTrue(properties.containsKey( + NodeFactory.createURI("http://iec.ch/TC57/CIM100#ClassA.floatProperty"))); + } + + { + var owlVersionIRIs = Set.of( + NodeFactory.createURI("http://example.org/MyCustomCore/1/1")); + + assertTrue(registry.containsProfile(owlVersionIRIs)); + var properties = registry.getPropertiesAndDatatypes(owlVersionIRIs); + assertNotNull(properties); + assertTrue(properties.containsKey( + NodeFactory.createURI("http://iec.ch/TC57/CIM100#ClassB.floatProperty"))); + } + + { + var owlVersionIRIs = Set.of( + NodeFactory.createURI("http://example.org/MyCustomOperation/1/1")); + + assertTrue(registry.containsProfile(owlVersionIRIs)); + var properties = registry.getPropertiesAndDatatypes(owlVersionIRIs); + assertNotNull(properties); + assertTrue(properties.containsKey( + NodeFactory.createURI("http://iec.ch/TC57/CIM100#ClassA.floatProperty"))); + } + + { + var owlVersionIRIs = Set.of( + NodeFactory.createURI("http://example.org/AnyOtherProfile/1/1")); + + assertFalse(registry.containsProfile(owlVersionIRIs)); + assertTrue(registry.getPropertiesAndDatatypes(owlVersionIRIs).isEmpty()); + } + } + +} \ No newline at end of file diff --git a/cimxml/src/test/java/de/soptim/opencgmes/cimxml/rdfs/TestCimProfileRegistryStdTypeMapping.java b/cimxml/src/test/java/de/soptim/opencgmes/cimxml/rdfs/TestCimProfileRegistryStdTypeMapping.java new file mode 100644 index 00000000..58f557e8 --- /dev/null +++ b/cimxml/src/test/java/de/soptim/opencgmes/cimxml/rdfs/TestCimProfileRegistryStdTypeMapping.java @@ -0,0 +1,203 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package de.soptim.opencgmes.cimxml.rdfs; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import de.soptim.opencgmes.cimxml.graph.CimProfile; +import de.soptim.opencgmes.cimxml.parser.ReaderCIMXML_StAX_SR; +import de.soptim.opencgmes.cimxml.parser.system.StreamCimXmlToDatasetGraph; +import java.io.StringReader; +import java.util.Set; +import org.apache.jena.datatypes.BaseDatatype; +import org.apache.jena.datatypes.TypeMapper; +import org.apache.jena.datatypes.xsd.XSDDatatype; +import org.apache.jena.graph.NodeFactory; +import org.junit.Test; + +public class TestCimProfileRegistryStdTypeMapping { + + /** + * The test is identical to + * de.soptim.opencgmes.cimxml.rdfs.TestCimProfileRegistryStd#registerProfileWithOneClassAndTwoSimpleProperties() + * except that not CIM primitive datatypes are referenced using 'cims:dataType', except the XML + * Schema datatypes are referenced directly using 'rdfs:range'. + */ + @Test + public void registerProfileWithOneClassAndTwoXSDProperties() { + final var rdfxml = """ + + + + + MYCUST + + 1.1.0 + + + + + + + + + + + + + + + + + + + + + + + + """; + + final var parser = new ReaderCIMXML_StAX_SR(); + final var streamRDF = new StreamCimXmlToDatasetGraph(); + + parser.read(new StringReader(rdfxml), streamRDF); + + var graph = streamRDF.getCimDatasetGraph().getDefaultGraph(); + + var profile = CimProfile.wrap(graph); + + var registry = new CimProfileRegistryStd(); + registry.register(profile); + + var owlVersionIRIs = Set.of(NodeFactory.createURI("http://example.org/MyCustom/1/1")); + + assertTrue(registry.containsProfile(owlVersionIRIs)); + + assertTrue(registry.getRegisteredProfiles().contains(profile)); + assertEquals(1, registry.getRegisteredProfiles().size()); + + var properties = registry.getPropertiesAndDatatypes(owlVersionIRIs); + assertNotNull(properties); + assertEquals(2, properties.size()); + + var floatProperty = NodeFactory.createURI("http://iec.ch/TC57/CIM100#ClassA.floatProperty"); + assertTrue(properties.containsKey(floatProperty)); + var propertyInfo = properties.get(floatProperty); + assertEquals(NodeFactory.createURI("http://iec.ch/TC57/CIM100#ClassA"), propertyInfo.rdfType()); + assertEquals(floatProperty, propertyInfo.property()); + assertEquals(XSDDatatype.XSDfloat, propertyInfo.primitiveType()); + assertNull(propertyInfo.referenceType()); + + var textProperty = NodeFactory.createURI("http://iec.ch/TC57/CIM100#ClassA.textProperty"); + assertTrue(properties.containsKey(textProperty)); + propertyInfo = properties.get(textProperty); + assertEquals(NodeFactory.createURI("http://iec.ch/TC57/CIM100#ClassA"), propertyInfo.rdfType()); + assertEquals(textProperty, propertyInfo.property()); + assertEquals(XSDDatatype.XSDstring, propertyInfo.primitiveType()); + assertNull(propertyInfo.referenceType()); + } + + @Test + public void registerProfileUsingCustomDataType() { + final var rdfxml = """ + + + + + MYCUST + + 1.1.0 + + + + + + + + + + + + + + + + + """; + + var semVerDataType = new BaseDatatype("https://semver.org#v2_0_0"); + var typeMapper = TypeMapper.getInstance(); + try { + typeMapper.registerDatatype(semVerDataType); + + final var parser = new ReaderCIMXML_StAX_SR(); + final var streamRDF = new StreamCimXmlToDatasetGraph(); + + parser.read(new StringReader(rdfxml), streamRDF); + + var graph = streamRDF.getCimDatasetGraph().getDefaultGraph(); + + var profile = CimProfile.wrap(graph); + + var registry = new CimProfileRegistryStd(); + registry.register(profile); + + var owlVersionIRIs = Set.of(NodeFactory.createURI("http://example.org/MyCustom/1/1")); + + assertTrue(registry.containsProfile(owlVersionIRIs)); + + assertTrue(registry.getRegisteredProfiles().contains(profile)); + assertEquals(1, registry.getRegisteredProfiles().size()); + + var properties = registry.getPropertiesAndDatatypes(owlVersionIRIs); + assertNotNull(properties); + assertEquals(1, properties.size()); + + var versionProperty = NodeFactory.createURI("http://iec.ch/TC57/CIM100#ClassA.version"); + assertTrue(properties.containsKey(versionProperty)); + var propertyInfo = properties.get(versionProperty); + assertEquals(NodeFactory.createURI("http://iec.ch/TC57/CIM100#ClassA"), + propertyInfo.rdfType()); + assertEquals(versionProperty, propertyInfo.property()); + assertEquals(semVerDataType, propertyInfo.primitiveType()); + assertNull(propertyInfo.referenceType()); + } finally { + typeMapper.unregisterDatatype(semVerDataType); + } + } + +} \ No newline at end of file diff --git a/cimxml/src/test/java/de/soptim/opencgmes/cimxml/sparql/core/CimDatasetGraphTest.java b/cimxml/src/test/java/de/soptim/opencgmes/cimxml/sparql/core/CimDatasetGraphTest.java deleted file mode 100644 index ef537f0c..00000000 --- a/cimxml/src/test/java/de/soptim/opencgmes/cimxml/sparql/core/CimDatasetGraphTest.java +++ /dev/null @@ -1,230 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package de.soptim.opencgmes.cimxml.sparql.core; - -import de.soptim.opencgmes.cimxml.parser.CimXmlParser; -import org.apache.jena.graph.NodeFactory; -import org.apache.jena.riot.system.ErrorHandlerFactory; -import org.junit.Test; - -import java.io.StringReader; - -import static org.junit.Assert.*; - -public class CimDatasetGraphTest { - - @Test - public void fullModelToSingleGraph() { - final var rdfxml = """ - - - - http://soptim.de/CIM/MyProfile/1.1 - - - My Custom Equipment - - - """; - - var model = new CimXmlParser(ErrorHandlerFactory.errorHandlerNoWarnings) - .parseCimModel(new StringReader(rdfxml)); - - var fullGraph = model.fullModelToSingleGraph(); - assertNotNull(fullGraph); - assertEquals(4, fullGraph.size()); - - assertTrue(fullGraph.contains( - NodeFactory.createURI("urn:uuid:08984e27-811f-4042-9125-1531ae0de0f6"), - NodeFactory.createURI("http://www.w3.org/1999/02/22-rdf-syntax-ns#type"), - NodeFactory.createURI("http://iec.ch/TC57/61970-552/ModelDescription/1#FullModel") - )); - assertTrue(fullGraph.contains( - NodeFactory.createURI("urn:uuid:08984e27-811f-4042-9125-1531ae0de0f6"), - NodeFactory.createURI("http://iec.ch/TC57/61970-552/ModelDescription/1#Model.profile"), - NodeFactory.createLiteralString("http://soptim.de/CIM/MyProfile/1.1") - )); - assertTrue(fullGraph.contains( - NodeFactory.createURI("urn:uuid:f67fc354-9e39-4191-a456-67537399bc48"), - NodeFactory.createURI("http://www.w3.org/1999/02/22-rdf-syntax-ns#type"), - NodeFactory.createURI("http://iec.ch/TC57/CIM100#MyEquipment") - )); - assertTrue(fullGraph.contains( - NodeFactory.createURI("urn:uuid:f67fc354-9e39-4191-a456-67537399bc48"), - NodeFactory.createURI("http://iec.ch/TC57/CIM100#IdentifiedObject.name"), - NodeFactory.createLiteralString("My Custom Equipment") - )); - } - - @Test - public void differenceModelToFullModel() { - final var rdfXmlFullModel = """ - - - - http://soptim.de/CIM/MyProfile/1.1 - - - Name of my element - A - - - Name of new element to remove entirely - property of new element to remove - - - Name of element to remain - property of new element to remain - - - """; - - final var rdfxmlDifferenceModel = """ - - - - http://soptim.de/CIM/MyProfile/1.1 - urn:uuid:d4336345-ad68-4566-afab-d9798ec5ca86 - - - - - Name of my element - - - - - - - - - B - - - - - Name of new element to add - property of new element - - - - - - - - - A - - - - - Name of new element to remove entirely - property of new element to remove - - - - - - - """; - - var predecessorFullModel = new CimXmlParser(ErrorHandlerFactory.errorHandlerNoWarnings) - .parseCimModel(new StringReader(rdfXmlFullModel)); - var differenceModel = new CimXmlParser(ErrorHandlerFactory.errorHandlerNoWarnings) - .parseCimModel(new StringReader(rdfxmlDifferenceModel)); - - var fullGraph = differenceModel.differenceModelToFullModel(predecessorFullModel); - assertNotNull(fullGraph); - assertEquals(9, fullGraph.size()); - // the element to remain unchanged - assertTrue(fullGraph.contains( - NodeFactory.createURI("urn:uuid:5a70f6b8-8c77-41f9-9793-6fe5bd67b756"), - NodeFactory.createURI("http://www.w3.org/1999/02/22-rdf-syntax-ns#type"), - NodeFactory.createURI("http://iec.ch/TC57/CIM100#MyElement") - )); - assertTrue(fullGraph.contains( - NodeFactory.createURI("urn:uuid:5a70f6b8-8c77-41f9-9793-6fe5bd67b756"), - NodeFactory.createURI("http://iec.ch/TC57/CIM100#IdentifiedObject.name"), - NodeFactory.createLiteralString("Name of element to remain") - )); - assertTrue(fullGraph.contains( - NodeFactory.createURI("urn:uuid:5a70f6b8-8c77-41f9-9793-6fe5bd67b756"), - NodeFactory.createURI("http://iec.ch/TC57/CIM100#MyElement.MyProperty"), - NodeFactory.createLiteralString("property of new element to remain") - )); - - // the unchanged properties - assertTrue(fullGraph.contains( - NodeFactory.createURI("urn:uuid:135c601e-bad4-4872-ba8f-b15baf91bd2f"), - NodeFactory.createURI("http://www.w3.org/1999/02/22-rdf-syntax-ns#type"), - NodeFactory.createURI("http://iec.ch/TC57/CIM100#MyElement") - )); - assertTrue(fullGraph.contains( - NodeFactory.createURI("urn:uuid:135c601e-bad4-4872-ba8f-b15baf91bd2f"), - NodeFactory.createURI("http://iec.ch/TC57/CIM100#IdentifiedObject.name"), - NodeFactory.createLiteralString("Name of my element") - )); - - // the updated property of the unchanged element - assertTrue(fullGraph.contains( - NodeFactory.createURI("urn:uuid:135c601e-bad4-4872-ba8f-b15baf91bd2f"), - NodeFactory.createURI("http://iec.ch/TC57/CIM100#MyElement.MyProperty"), - NodeFactory.createLiteralString("B") - )); - - // the newly added element - assertTrue(fullGraph.contains( - NodeFactory.createURI("urn:uuid:2d1e4820-8858-49de-b441-5a03e7c40035"), - NodeFactory.createURI("http://www.w3.org/1999/02/22-rdf-syntax-ns#type"), - NodeFactory.createURI("http://iec.ch/TC57/CIM100#MyElement") - )); - assertTrue(fullGraph.contains( - NodeFactory.createURI("urn:uuid:2d1e4820-8858-49de-b441-5a03e7c40035"), - NodeFactory.createURI("http://iec.ch/TC57/CIM100#IdentifiedObject.name"), - NodeFactory.createLiteralString("Name of new element to add") - )); - assertTrue(fullGraph.contains( - NodeFactory.createURI("urn:uuid:2d1e4820-8858-49de-b441-5a03e7c40035"), - NodeFactory.createURI("http://iec.ch/TC57/CIM100#MyElement.MyProperty"), - NodeFactory.createLiteralString("property of new element") - )); - - // the removed element must not be present - assertFalse(fullGraph.contains( - NodeFactory.createURI("urn:uuid:c9fe6664-fcf0-44e6-9d20-656538b68d1c"), - NodeFactory.createURI("http://www.w3.org/1999/02/22-rdf-syntax-ns#type"), - NodeFactory.createURI("http://iec.ch/TC57/CIM100#MyElement") - )); - assertFalse(fullGraph.contains( - NodeFactory.createURI("urn:uuid:c9fe6664-fcf0-44e6-9d20-656538b68d1c"), - NodeFactory.createURI("http://iec.ch/TC57/CIM100#IdentifiedObject.name"), - NodeFactory.createLiteralString("Name of new element to remove entirely") - )); - assertFalse(fullGraph.contains( - NodeFactory.createURI("urn:uuid:c9fe6664-fcf0-44e6-9d20-656538b68d1c"), - NodeFactory.createURI("http://iec.ch/TC57/CIM100#MyElement.MyProperty"), - NodeFactory.createLiteralString("property of new element to remove") - )); - } -} \ No newline at end of file diff --git a/cimxml/src/test/java/de/soptim/opencgmes/cimxml/sparql/core/TestCimDatasetGraph.java b/cimxml/src/test/java/de/soptim/opencgmes/cimxml/sparql/core/TestCimDatasetGraph.java new file mode 100644 index 00000000..e4b1706e --- /dev/null +++ b/cimxml/src/test/java/de/soptim/opencgmes/cimxml/sparql/core/TestCimDatasetGraph.java @@ -0,0 +1,232 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package de.soptim.opencgmes.cimxml.sparql.core; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import de.soptim.opencgmes.cimxml.parser.CimXmlParser; +import java.io.StringReader; +import org.apache.jena.graph.NodeFactory; +import org.apache.jena.riot.system.ErrorHandlerFactory; +import org.junit.Test; + +public class TestCimDatasetGraph { + + @Test + public void fullModelToSingleGraph() { + final var rdfxml = """ + + + + http://soptim.de/CIM/MyProfile/1.1 + + + My Custom Equipment + + + """; + + var model = new CimXmlParser(ErrorHandlerFactory.errorHandlerNoWarnings) + .parseCimModel(new StringReader(rdfxml)); + + var fullGraph = model.fullModelToSingleGraph(); + assertNotNull(fullGraph); + assertEquals(4, fullGraph.size()); + + assertTrue(fullGraph.contains( + NodeFactory.createURI("urn:uuid:08984e27-811f-4042-9125-1531ae0de0f6"), + NodeFactory.createURI("http://www.w3.org/1999/02/22-rdf-syntax-ns#type"), + NodeFactory.createURI("http://iec.ch/TC57/61970-552/ModelDescription/1#FullModel") + )); + assertTrue(fullGraph.contains( + NodeFactory.createURI("urn:uuid:08984e27-811f-4042-9125-1531ae0de0f6"), + NodeFactory.createURI("http://iec.ch/TC57/61970-552/ModelDescription/1#Model.profile"), + NodeFactory.createLiteralString("http://soptim.de/CIM/MyProfile/1.1") + )); + assertTrue(fullGraph.contains( + NodeFactory.createURI("urn:uuid:f67fc354-9e39-4191-a456-67537399bc48"), + NodeFactory.createURI("http://www.w3.org/1999/02/22-rdf-syntax-ns#type"), + NodeFactory.createURI("http://iec.ch/TC57/CIM100#MyEquipment") + )); + assertTrue(fullGraph.contains( + NodeFactory.createURI("urn:uuid:f67fc354-9e39-4191-a456-67537399bc48"), + NodeFactory.createURI("http://iec.ch/TC57/CIM100#IdentifiedObject.name"), + NodeFactory.createLiteralString("My Custom Equipment") + )); + } + + @Test + public void differenceModelToFullModel() { + final var rdfXmlFullModel = """ + + + + http://soptim.de/CIM/MyProfile/1.1 + + + Name of my element + A + + + Name of new element to remove entirely + property of new element to remove + + + Name of element to remain + property of new element to remain + + + """; + + final var rdfxmlDifferenceModel = """ + + + + http://soptim.de/CIM/MyProfile/1.1 + urn:uuid:d4336345-ad68-4566-afab-d9798ec5ca86 + + + + + Name of my element + + + + + + + + + B + + + + + Name of new element to add + property of new element + + + + + + + + + A + + + + + Name of new element to remove entirely + property of new element to remove + + + + + + + """; + + var predecessorFullModel = new CimXmlParser(ErrorHandlerFactory.errorHandlerNoWarnings) + .parseCimModel(new StringReader(rdfXmlFullModel)); + var differenceModel = new CimXmlParser(ErrorHandlerFactory.errorHandlerNoWarnings) + .parseCimModel(new StringReader(rdfxmlDifferenceModel)); + + var fullGraph = differenceModel.differenceModelToFullModel(predecessorFullModel); + assertNotNull(fullGraph); + assertEquals(9, fullGraph.size()); + // the element to remain unchanged + assertTrue(fullGraph.contains( + NodeFactory.createURI("urn:uuid:5a70f6b8-8c77-41f9-9793-6fe5bd67b756"), + NodeFactory.createURI("http://www.w3.org/1999/02/22-rdf-syntax-ns#type"), + NodeFactory.createURI("http://iec.ch/TC57/CIM100#MyElement") + )); + assertTrue(fullGraph.contains( + NodeFactory.createURI("urn:uuid:5a70f6b8-8c77-41f9-9793-6fe5bd67b756"), + NodeFactory.createURI("http://iec.ch/TC57/CIM100#IdentifiedObject.name"), + NodeFactory.createLiteralString("Name of element to remain") + )); + assertTrue(fullGraph.contains( + NodeFactory.createURI("urn:uuid:5a70f6b8-8c77-41f9-9793-6fe5bd67b756"), + NodeFactory.createURI("http://iec.ch/TC57/CIM100#MyElement.MyProperty"), + NodeFactory.createLiteralString("property of new element to remain") + )); + + // the unchanged properties + assertTrue(fullGraph.contains( + NodeFactory.createURI("urn:uuid:135c601e-bad4-4872-ba8f-b15baf91bd2f"), + NodeFactory.createURI("http://www.w3.org/1999/02/22-rdf-syntax-ns#type"), + NodeFactory.createURI("http://iec.ch/TC57/CIM100#MyElement") + )); + assertTrue(fullGraph.contains( + NodeFactory.createURI("urn:uuid:135c601e-bad4-4872-ba8f-b15baf91bd2f"), + NodeFactory.createURI("http://iec.ch/TC57/CIM100#IdentifiedObject.name"), + NodeFactory.createLiteralString("Name of my element") + )); + + // the updated property of the unchanged element + assertTrue(fullGraph.contains( + NodeFactory.createURI("urn:uuid:135c601e-bad4-4872-ba8f-b15baf91bd2f"), + NodeFactory.createURI("http://iec.ch/TC57/CIM100#MyElement.MyProperty"), + NodeFactory.createLiteralString("B") + )); + + // the newly added element + assertTrue(fullGraph.contains( + NodeFactory.createURI("urn:uuid:2d1e4820-8858-49de-b441-5a03e7c40035"), + NodeFactory.createURI("http://www.w3.org/1999/02/22-rdf-syntax-ns#type"), + NodeFactory.createURI("http://iec.ch/TC57/CIM100#MyElement") + )); + assertTrue(fullGraph.contains( + NodeFactory.createURI("urn:uuid:2d1e4820-8858-49de-b441-5a03e7c40035"), + NodeFactory.createURI("http://iec.ch/TC57/CIM100#IdentifiedObject.name"), + NodeFactory.createLiteralString("Name of new element to add") + )); + assertTrue(fullGraph.contains( + NodeFactory.createURI("urn:uuid:2d1e4820-8858-49de-b441-5a03e7c40035"), + NodeFactory.createURI("http://iec.ch/TC57/CIM100#MyElement.MyProperty"), + NodeFactory.createLiteralString("property of new element") + )); + + // the removed element must not be present + assertFalse(fullGraph.contains( + NodeFactory.createURI("urn:uuid:c9fe6664-fcf0-44e6-9d20-656538b68d1c"), + NodeFactory.createURI("http://www.w3.org/1999/02/22-rdf-syntax-ns#type"), + NodeFactory.createURI("http://iec.ch/TC57/CIM100#MyElement") + )); + assertFalse(fullGraph.contains( + NodeFactory.createURI("urn:uuid:c9fe6664-fcf0-44e6-9d20-656538b68d1c"), + NodeFactory.createURI("http://iec.ch/TC57/CIM100#IdentifiedObject.name"), + NodeFactory.createLiteralString("Name of new element to remove entirely") + )); + assertFalse(fullGraph.contains( + NodeFactory.createURI("urn:uuid:c9fe6664-fcf0-44e6-9d20-656538b68d1c"), + NodeFactory.createURI("http://iec.ch/TC57/CIM100#MyElement.MyProperty"), + NodeFactory.createLiteralString("property of new element to remove") + )); + } +} \ No newline at end of file diff --git a/cimxml/src/test/java/de/soptim/opencgmes/parser/TestConvertCimXmlToJsonLd.java b/cimxml/src/test/java/de/soptim/opencgmes/parser/TestConvertCimXmlToJsonLd.java index d39a52ca..d587219b 100644 --- a/cimxml/src/test/java/de/soptim/opencgmes/parser/TestConvertCimXmlToJsonLd.java +++ b/cimxml/src/test/java/de/soptim/opencgmes/parser/TestConvertCimXmlToJsonLd.java @@ -21,118 +21,127 @@ import de.soptim.opencgmes.cimxml.graph.CimProfile; import de.soptim.opencgmes.cimxml.parser.CimXmlParser; import de.soptim.opencgmes.cimxml.sparql.core.LinkedCimDatasetGraph; +import java.io.BufferedOutputStream; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; import org.apache.jena.graph.NodeFactory; import org.apache.jena.riot.Lang; import org.apache.jena.riot.RDFDataMgr; import org.junit.Ignore; import org.junit.Test; -import java.io.BufferedOutputStream; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; - public class TestConvertCimXmlToJsonLd { - private static final String CIM_SCHEMA_IRI = "cgmes:schema:"; - private static final String CIM_MODEL_IRI = "cgmes:model:"; - private static final String CIM_HEADER_IRI = "cgmes:header:"; - //private static final String CIM_SHACL_IRI = "cgmes:shacl:"; - //private static final String CIM_VALIDATION_REPORT_IRI = "cgmes:validationReport:"; - - public void convertCimXmlToJsonLd(String rdfsSchemaFolder, String cimXmlFolder, String jsonldOutputFile) { - convertCimXmlToJsonLd(Path.of(rdfsSchemaFolder), Path.of(cimXmlFolder), Path.of(jsonldOutputFile)); - } + private static final String CIM_SCHEMA_IRI = "cgmes:schema:"; + private static final String CIM_MODEL_IRI = "cgmes:model:"; + private static final String CIM_HEADER_IRI = "cgmes:header:"; + //private static final String CIM_SHACL_IRI = "cgmes:shacl:"; + //private static final String CIM_VALIDATION_REPORT_IRI = "cgmes:validationReport:"; - public void convertCimXmlToJsonLd(Path rdfsSchemaFolder, Path cimXmlFolder, Path jsonldOutputFile) { - final var dataset = new LinkedCimDatasetGraph(); - final var parser = new CimXmlParser(); - try (var rdfFiles = Files.list(rdfsSchemaFolder)) { - rdfFiles.parallel() - .filter(f -> f.toString().endsWith(".rdf")) - .forEach(rdfFile -> { - try { - final var schema = parser.parseAndRegisterCimProfile(rdfFile); - // add only header profiles to the dataset. - // the other profiles are only added when referenced in the dataset. - if(schema.isHeaderProfile()) { - dataset.addGraph(NodeFactory.createURI(CIM_SCHEMA_IRI + schema.getDcatKeyword()), schema); - dataset.prefixes().putAll(schema.getPrefixMapping()); - } - } catch (IOException e) { - throw new RuntimeException(e); - } - }); - } catch (IOException e) { - throw new RuntimeException(e); - } + public void convertCimXmlToJsonLd(String rdfsSchemaFolder, String cimXmlFolder, + String jsonldOutputFile) { + convertCimXmlToJsonLd(Path.of(rdfsSchemaFolder), Path.of(cimXmlFolder), + Path.of(jsonldOutputFile)); + } - try (var cimXmlFiles = Files.list(cimXmlFolder)) { - cimXmlFiles.parallel() - .filter(f -> f.toString().endsWith(".xml")) - .forEach(cimXmlFile -> { - try { - final var cimDataset = parser.parseCimModel(cimXmlFile); - if(!cimDataset.isFullModel()) { - System.out.println("File " + cimXmlFile.getFileName() + " is not a FullModel. Skipping."); - return; - } - final var keywords = parser.getCimProfileRegistry() - .getMatchingProfiles(cimDataset.getModelHeader().getProfiles()) - .stream() - .peek(schema -> { - final var schemaGraphName = NodeFactory.createURI( - CIM_SCHEMA_IRI + schema.getDcatKeyword()); - if(!dataset.containsGraph(schemaGraphName)) { - dataset.addGraph(schemaGraphName, schema); - dataset.prefixes().putAll(schema.getPrefixMapping()); - } - }) - .map(CimProfile::getDcatKeyword) - .distinct() - .sorted() - .toList(); - if(keywords.isEmpty()) { - System.out.println("File " + cimXmlFile.getFileName() + " does not match any profile. Skipping."); - return; - } - var keyword = keywords.getFirst(); - if(keywords.size() > 1) { - System.out.println("File " + cimXmlFile.getFileName() + " matches multiple profiles: " + keywords + ". Using first one: " + keyword); - } - dataset.addGraph(NodeFactory.createURI(CIM_HEADER_IRI + keyword), cimDataset.getModelHeader()); - dataset.addGraph(NodeFactory.createURI(CIM_MODEL_IRI + keyword), cimDataset.getBody()); - dataset.prefixes().putAll(cimDataset.prefixes()); - } catch (IOException e) { - throw new RuntimeException(e); - } - }); - } catch (IOException e) { - throw new RuntimeException(e); - } - // create java nio file and output stream - try (var rdfFiles = new BufferedOutputStream(Files.newOutputStream(jsonldOutputFile))) { - RDFDataMgr.write(rdfFiles, dataset, Lang.JSONLD11); - } catch (IOException e) { - throw new RuntimeException(e); - } + public void convertCimXmlToJsonLd(Path rdfsSchemaFolder, Path cimXmlFolder, + Path jsonldOutputFile) { + final var dataset = new LinkedCimDatasetGraph(); + final var parser = new CimXmlParser(); + try (var rdfFiles = Files.list(rdfsSchemaFolder)) { + rdfFiles.parallel() + .filter(f -> f.toString().endsWith(".rdf")) + .forEach(rdfFile -> { + try { + final var schema = parser.parseAndRegisterCimProfile(rdfFile); + // add only header profiles to the dataset. + // the other profiles are only added when referenced in the dataset. + if (schema.isHeaderProfile()) { + dataset.addGraph(NodeFactory.createURI(CIM_SCHEMA_IRI + schema.getDcatKeyword()), + schema); + dataset.prefixes().putAll(schema.getPrefixMapping()); + } + } catch (IOException e) { + throw new RuntimeException(e); + } + }); + } catch (IOException e) { + throw new RuntimeException(e); } - @Ignore - @Test - public void convertFullGridMerged() { - convertCimXmlToJsonLd( - "../data/ApplicationProfilesLibrary-release1-1-0/CGMES/RDFS/", - "../data/CGMES_ConformityAssessmentScheme_r3-0-2/CGMES_ConformityAssessmentScheme_TestConfigurations_v3-0-3/v3.0/FullGrid/FullGrid-Merged/", - "../output/FullGrid-Merged.jsonld"); + try (var cimXmlFiles = Files.list(cimXmlFolder)) { + cimXmlFiles.parallel() + .filter(f -> f.toString().endsWith(".xml")) + .forEach(cimXmlFile -> { + try { + final var cimDataset = parser.parseCimModel(cimXmlFile); + if (!cimDataset.isFullModel()) { + System.out.println( + "File " + cimXmlFile.getFileName() + " is not a FullModel. Skipping."); + return; + } + final var keywords = parser.getCimProfileRegistry() + .getMatchingProfiles(cimDataset.getModelHeader().getProfiles()) + .stream() + .peek(schema -> { + final var schemaGraphName = NodeFactory.createURI( + CIM_SCHEMA_IRI + schema.getDcatKeyword()); + if (!dataset.containsGraph(schemaGraphName)) { + dataset.addGraph(schemaGraphName, schema); + dataset.prefixes().putAll(schema.getPrefixMapping()); + } + }) + .map(CimProfile::getDcatKeyword) + .distinct() + .sorted() + .toList(); + if (keywords.isEmpty()) { + System.out.println( + "File " + cimXmlFile.getFileName() + " does not match any profile. Skipping."); + return; + } + var keyword = keywords.getFirst(); + if (keywords.size() > 1) { + System.out.println( + "File " + cimXmlFile.getFileName() + " matches multiple profiles: " + keywords + + ". Using first one: " + keyword); + } + dataset.addGraph(NodeFactory.createURI(CIM_HEADER_IRI + keyword), + cimDataset.getModelHeader()); + dataset.addGraph(NodeFactory.createURI(CIM_MODEL_IRI + keyword), + cimDataset.getBody()); + dataset.prefixes().putAll(cimDataset.prefixes()); + } catch (IOException e) { + throw new RuntimeException(e); + } + }); + } catch (IOException e) { + throw new RuntimeException(e); } - - @Ignore - @Test - public void convertRealGridMerged() { - convertCimXmlToJsonLd( - "../data/ApplicationProfilesLibrary-release1-1-0/CGMES/RDFS/", - "../data/CGMES_ConformityAssessmentScheme_r3-0-2/CGMES_ConformityAssessmentScheme_TestConfigurations_v3-0-3/v3.0/RealGrid/RealGrid-Merged/", - "../output/RealGrid-Merged.jsonld"); + // create java nio file and output stream + try (var rdfFiles = new BufferedOutputStream(Files.newOutputStream(jsonldOutputFile))) { + RDFDataMgr.write(rdfFiles, dataset, Lang.JSONLD11); + } catch (IOException e) { + throw new RuntimeException(e); } + } + + @Ignore + @Test + public void convertFullGridMerged() { + convertCimXmlToJsonLd( + "../data/ApplicationProfilesLibrary-release1-1-0/CGMES/RDFS/", + "../data/CGMES_ConformityAssessmentScheme_r3-0-2/CGMES_ConformityAssessmentScheme_TestConfigurations_v3-0-3/v3.0/FullGrid/FullGrid-Merged/", + "../output/FullGrid-Merged.jsonld"); + } + + @Ignore + @Test + public void convertRealGridMerged() { + convertCimXmlToJsonLd( + "../data/ApplicationProfilesLibrary-release1-1-0/CGMES/RDFS/", + "../data/CGMES_ConformityAssessmentScheme_r3-0-2/CGMES_ConformityAssessmentScheme_TestConfigurations_v3-0-3/v3.0/RealGrid/RealGrid-Merged/", + "../output/RealGrid-Merged.jsonld"); + } } \ No newline at end of file From c50db922647195a934b6cc9f8eb80633d684ed1b Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 7 Mar 2026 22:43:08 +0000 Subject: [PATCH 3/4] Add ENTSO-E application-profiles-library as test submodule with unit tests Add the ENTSO-E application-profiles-library (v1.1.1) as a Git submodule under cimxml/testing/ for testing purposes only. Create parameterized unit tests that recursively discover and load all .rdf schema files per directory, verifying that profiles can be parsed, registered, and contain expected metadata (version IRIs, keywords, non-empty graphs). Excluded directories: NCP/PastReleases and CGMES/CurrentRelease/RDFS/Beta_501_Ed2_CD. Added Apache 2.0 attribution in README.md and pom.xml. Add datatype coverage and cross-version compatibility tests - TestApplicationProfilesLibrary: add datatypeCoverageNonEmptyPropertyMaps test verifying getPropertiesAndDatatypes() returns non-empty maps for each registered profile. Fix exclusions for SHACL/RDF and NCP/PROF directories. Handle duplicate profile registration gracefully for directories containing overlapping profiles (CGMES 2.4 augmented equipment, multiple header files). - TestCrossVersionProfileCompatibility: new test class loading CGMES 2.4 and 3.0 profiles into a single registry, verifying coexistence across both CIM versions. Updated .gitignore to ignore any .iml files. --- .gitmodules | 3 + .idea/.gitignore | 2 +- README.md | 6 + cimxml/pom.xml | 6 + .../rdfs/TestApplicationProfilesLibrary.java | 248 ++++++++++++++++++ .../TestCrossVersionProfileCompatibility.java | 130 +++++++++ cimxml/testing/application-profiles-library | 1 + 7 files changed, 395 insertions(+), 1 deletion(-) create mode 100644 .gitmodules create mode 100644 cimxml/src/test/java/de/soptim/opencgmes/cimxml/rdfs/TestApplicationProfilesLibrary.java create mode 100644 cimxml/src/test/java/de/soptim/opencgmes/cimxml/rdfs/TestCrossVersionProfileCompatibility.java create mode 160000 cimxml/testing/application-profiles-library diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 00000000..f74786db --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "cimxml/testing/application-profiles-library"] + path = cimxml/testing/application-profiles-library + url = https://github.com/entsoe/application-profiles-library.git diff --git a/.idea/.gitignore b/.idea/.gitignore index 22db6617..a514b6e8 100644 --- a/.idea/.gitignore +++ b/.idea/.gitignore @@ -19,5 +19,5 @@ /kotlinc.xml /misc.xml /modules.xml -/OpenCGMES.iml /vcs.xml +/*.iml diff --git a/README.md b/README.md index 8d48ab75..de4934fe 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,12 @@ A web application for uploading RDF Schema, SHACL shapes, and CIM XML files, querying the data using SPARQL, and validating the data against SHACL shapes. see [QueryAndValidationUI](./QueryAndValidationUI/README.md) +## Third-Party Test Data + +This project includes the [ENTSO-E Application Profiles Library](https://github.com/entsoe/application-profiles-library) +(v1.1.1) as a Git submodule under `cimxml/testing/application-profiles-library/` for testing purposes only. +The Application Profiles Library is licensed under the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). + ## License This project is licensed under the [Apache License 2.0](LICENSE). diff --git a/cimxml/pom.xml b/cimxml/pom.xml index 41b1e124..2ae62b23 100644 --- a/cimxml/pom.xml +++ b/cimxml/pom.xml @@ -20,6 +20,12 @@ Copyright © 2004 World Wide Web Consortium Used under W3C Software License for testing purposes only --> + diff --git a/cimxml/src/test/java/de/soptim/opencgmes/cimxml/rdfs/TestApplicationProfilesLibrary.java b/cimxml/src/test/java/de/soptim/opencgmes/cimxml/rdfs/TestApplicationProfilesLibrary.java new file mode 100644 index 00000000..b7096c4b --- /dev/null +++ b/cimxml/src/test/java/de/soptim/opencgmes/cimxml/rdfs/TestApplicationProfilesLibrary.java @@ -0,0 +1,248 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package de.soptim.opencgmes.cimxml.rdfs; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.junit.Assume.assumeTrue; + +import de.soptim.opencgmes.cimxml.graph.CimProfile; +import de.soptim.opencgmes.cimxml.parser.CimXmlParser; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.apache.jena.graph.Node; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Tests that verify all RDF Schema files from the ENTSO-E application-profiles-library can be + * loaded and registered by the CimXmlParser. + * + *

      The application-profiles-library is included as a Git submodule at + * {@code cimxml/testing/application-profiles-library} and is licensed under Apache License 2.0. + * See + * https://github.com/entsoe/application-profiles-library

      + * + *

      Each test case corresponds to one directory containing {@code .rdf} files. A fresh + * {@link CimXmlParser} instance is created per directory, and all {@code .rdf} files in that + * directory are loaded and registered as CIM profiles.

      + */ +@RunWith(Parameterized.class) +public class TestApplicationProfilesLibrary { + + private static final Logger LOG = + LoggerFactory.getLogger(TestApplicationProfilesLibrary.class); + + private static final Path PROFILES_ROOT = + Paths.get("testing", "application-profiles-library"); + + private static final List EXCLUDED_PATHS = List.of( + "NCP/PastReleases", + "CGMES/CurrentRelease/RDFS/Beta_501_Ed2_CD", + // SHACL constraint files (.rdf format) are not RDFS vocabulary profiles + "CGMES/CurrentRelease/SHACL", + // PROF files do not define a 'cim' namespace and are not CIM RDFS profiles + "NCP/CurrentRelease/PROF" + ); + + private final String directoryName; + private final Path directoryPath; + + public TestApplicationProfilesLibrary(String directoryName, Path directoryPath) { + this.directoryName = directoryName; + this.directoryPath = directoryPath; + } + + @Parameterized.Parameters(name = "{0}") + public static Collection discoverProfileDirectories() throws IOException { + List testCases = new ArrayList<>(); + + if (!Files.exists(PROFILES_ROOT) || !Files.isDirectory(PROFILES_ROOT)) { + testCases.add(new Object[]{ + "application-profiles-library submodule not available", null + }); + return testCases; + } + + try (Stream paths = Files.walk(PROFILES_ROOT)) { + List rdfFiles = paths + .filter(Files::isRegularFile) + .filter(p -> p.toString().endsWith(".rdf")) + .filter(p -> !isExcluded(p)) + .collect(Collectors.toList()); + + rdfFiles.stream() + .map(Path::getParent) + .distinct() + .sorted() + .forEach(dir -> { + String displayName = PROFILES_ROOT.relativize(dir).toString(); + testCases.add(new Object[]{displayName, dir}); + }); + } + + if (testCases.isEmpty()) { + testCases.add(new Object[]{"No .rdf files found", null}); + } + + return testCases; + } + + private static boolean isExcluded(Path path) { + String normalized = PROFILES_ROOT.relativize(path).toString().replace('\\', '/'); + return EXCLUDED_PATHS.stream().anyMatch(normalized::startsWith); + } + + private static List listRdfFiles(Path directory) throws IOException { + try (Stream paths = Files.list(directory)) { + return paths + .filter(Files::isRegularFile) + .filter(p -> p.toString().endsWith(".rdf")) + .sorted() + .collect(Collectors.toList()); + } + } + + /** + * Loads all .rdf files in the directory into the given parser. Some directories contain + * profiles with overlapping version IRIs (e.g. CGMES 2.4 "Augmented" equipment profiles) + * or multiple header profiles for the same CIM namespace. These duplicate registrations are + * logged but not treated as failures, since the files themselves parse correctly. + */ + private static CimXmlParser loadProfiles(Path directory) throws IOException { + CimXmlParser parser = new CimXmlParser(); + for (Path rdfFile : listRdfFiles(directory)) { + try { + parser.parseAndRegisterCimProfile(rdfFile); + } catch (IllegalArgumentException e) { + if (e.getMessage() != null && (e.getMessage().contains("already registered"))) { + LOG.info("Skipping duplicate profile {}: {}", rdfFile.getFileName(), e.getMessage()); + } else { + throw e; + } + } + } + return parser; + } + + @Test + public void loadAllRdfSchemaFilesInDirectory() throws IOException { + assumeTrue("Submodule not available, skipping test: " + directoryName, + directoryPath != null); + + List rdfFiles = listRdfFiles(directoryPath); + assertFalse("No .rdf files found in " + directoryName, rdfFiles.isEmpty()); + + CimXmlParser parser = loadProfiles(directoryPath); + + assertFalse("No profiles were registered from " + directoryName, + parser.getCimProfileRegistry().getRegisteredProfiles().isEmpty()); + } + + @Test + public void allProfilesHaveNonEmptyGraph() throws IOException { + assumeTrue("Submodule not available, skipping test: " + directoryName, + directoryPath != null); + + CimXmlParser parser = loadProfiles(directoryPath); + + for (CimProfile profile : parser.getCimProfileRegistry().getRegisteredProfiles()) { + assertTrue("Profile graph should not be empty: " + profile.getDcatKeyword(), + profile.size() > 0); + assertNotNull("Profile should have a CIM namespace", + profile.getCimNamespace()); + } + } + + @Test + public void allNonHeaderProfilesHaveVersionIris() throws IOException { + assumeTrue("Submodule not available, skipping test: " + directoryName, + directoryPath != null); + + CimXmlParser parser = loadProfiles(directoryPath); + + for (CimProfile profile : parser.getCimProfileRegistry().getRegisteredProfiles()) { + if (!profile.isHeaderProfile()) { + assertNotNull("Non-header profile should have version IRIs: " + + profile.getDcatKeyword(), profile.getOwlVersionIris()); + assertFalse("Non-header profile should have at least one version IRI: " + + profile.getDcatKeyword(), profile.getOwlVersionIris().isEmpty()); + } + } + } + + @Test + public void allNonHeaderProfilesHaveKeyword() throws IOException { + assumeTrue("Submodule not available, skipping test: " + directoryName, + directoryPath != null); + + CimXmlParser parser = loadProfiles(directoryPath); + + for (CimProfile profile : parser.getCimProfileRegistry().getRegisteredProfiles()) { + if (!profile.isHeaderProfile()) { + assertNotNull("Non-header profile should have a dcat:keyword: " + + profile.getOwlVersionIris(), + profile.getDcatKeyword()); + assertFalse("Non-header profile keyword should not be empty: " + + profile.getOwlVersionIris(), + profile.getDcatKeyword().isEmpty()); + } + } + } + + @Test + public void datatypeCoverageNonEmptyPropertyMaps() throws IOException { + assumeTrue("Submodule not available, skipping test: " + directoryName, + directoryPath != null); + + CimXmlParser parser = loadProfiles(directoryPath); + CimProfileRegistry registry = parser.getCimProfileRegistry(); + + for (CimProfile profile : registry.getRegisteredProfiles()) { + if (profile.isHeaderProfile()) { + var headerProps = registry.getHeaderPropertiesAndDatatypes( + profile.getCimNamespace()); + assertNotNull("Header property map should not be null for " + + profile.getCimNamespace(), headerProps); + assertFalse("Header property map should not be empty for " + + profile.getCimNamespace(), headerProps.isEmpty()); + } else { + Set versionIris = profile.getOwlVersionIris(); + var props = registry.getPropertiesAndDatatypes(versionIris); + assertNotNull("Property map should not be null for " + + profile.getDcatKeyword(), props); + assertFalse("Property map should not be empty for " + + profile.getDcatKeyword(), props.isEmpty()); + } + } + } +} diff --git a/cimxml/src/test/java/de/soptim/opencgmes/cimxml/rdfs/TestCrossVersionProfileCompatibility.java b/cimxml/src/test/java/de/soptim/opencgmes/cimxml/rdfs/TestCrossVersionProfileCompatibility.java new file mode 100644 index 00000000..1801a6b3 --- /dev/null +++ b/cimxml/src/test/java/de/soptim/opencgmes/cimxml/rdfs/TestCrossVersionProfileCompatibility.java @@ -0,0 +1,130 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package de.soptim.opencgmes.cimxml.rdfs; + +import static org.junit.Assert.assertTrue; +import static org.junit.Assume.assumeTrue; + +import de.soptim.opencgmes.cimxml.graph.CimProfile; +import de.soptim.opencgmes.cimxml.parser.CimXmlParser; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.junit.Before; +import org.junit.Test; + +/** + * Tests cross-version compatibility by loading CGMES 2.4 (CIM 16) and CGMES 3.0 (CIM 17/18) + * profiles into the same CimProfileRegistry and verifying they coexist. + * + *

      CGMES 2.4 profiles use {@code http://iec.ch/TC57/2013/CIM-schema-cim16#} as the CIM + * namespace, while CGMES 3.0 profiles use {@code http://iec.ch/TC57/CIM100#}.

      + */ +public class TestCrossVersionProfileCompatibility { + + private static final Path PROFILES_ROOT = + Paths.get("testing", "application-profiles-library"); + + private static final Path CGMES_30_RDFS = + PROFILES_ROOT.resolve("CGMES/CurrentRelease/RDFS"); + + private static final Path CGMES_24_RDFS = + PROFILES_ROOT.resolve("CGMES/PastReleases/v2-4/Original/RDFS"); + + private boolean submoduleAvailable; + + @Before + public void checkSubmodule() { + submoduleAvailable = Files.exists(PROFILES_ROOT) && Files.isDirectory(PROFILES_ROOT) + && Files.exists(CGMES_30_RDFS) && Files.exists(CGMES_24_RDFS); + } + + private static List listRdfFiles(Path directory) throws IOException { + try (Stream paths = Files.list(directory)) { + return paths + .filter(Files::isRegularFile) + .filter(p -> p.toString().endsWith(".rdf")) + .sorted() + .collect(Collectors.toList()); + } + } + + @Test + public void cgmes24And30CoexistInSameRegistry() throws IOException { + assumeTrue("Submodule not available", submoduleAvailable); + + CimXmlParser parser = new CimXmlParser(); + + // Load CGMES 3.0 profiles first (skip second header to avoid duplicate) + for (Path rdfFile : listRdfFiles(CGMES_30_RDFS)) { + if (rdfFile.getFileName().toString().contains("RDFS2019")) { + continue; // Skip the older RDFS2019 header; RDFS2020 header will be loaded + } + parser.parseAndRegisterCimProfile(rdfFile); + } + + int cgmes30Count = parser.getCimProfileRegistry().getRegisteredProfiles().size(); + assertTrue("Should have registered CGMES 3.0 profiles", cgmes30Count > 0); + + // Load CGMES 2.4 base profiles (skip augmented supersets to avoid duplicate IRIs) + for (Path rdfFile : listRdfFiles(CGMES_24_RDFS)) { + String name = rdfFile.getFileName().toString(); + // Skip profiles that list all verionIRIs but contain less, because they then conflict with + // the more complete profiles that are also in the directory. This is a known issue with some + // CGMES 2.4 "Augmented" equipment profiles, which list all version IRIs but only contain a + // subset of the properties. Since these files parse correctly and the registry handles + // duplicate IRIs by ignoring subsequent registrations, we can skip them in this test to + // focus on verifying that the unique profiles from both versions coexist without conflicts. + if (name.endsWith("EquipmentProfileCoreOperationRDFSAugmented-v2_4_15-4Sep2020.rdf") + || name.endsWith("EquipmentProfileCoreRDFSAugmented-v2_4_15-4Sep2020.rdf") + || name.endsWith("EquipmentProfileCoreShortCircuitRDFSAugmented-v2_4_15-4Sep2020.rdf")) { + continue; + } + // print file name for debugging + System.out.println("Before loading: " + rdfFile.getFileName().toString()); + var profile = parser.parseAndRegisterCimProfile(rdfFile); + // print concatenated version IRIs for loaded file for debugging + var versionIris = profile.getOwlVersionIris().stream() + .map(Object::toString) + .collect(Collectors.joining(", ")); + System.out.println("Registered version IRIs after loading: " + versionIris); + } + + Set allProfiles = parser.getCimProfileRegistry().getRegisteredProfiles(); + int totalCount = allProfiles.size(); + assertTrue("Should have registered more profiles after adding CGMES 2.4", + totalCount > cgmes30Count); + + // Verify both CIM namespaces are present + Set namespaces = new HashSet<>(); + for (CimProfile profile : allProfiles) { + namespaces.add(profile.getCimNamespace()); + } + assertTrue("Should contain CIM 16 namespace (CGMES 2.4)", + namespaces.stream().anyMatch(ns -> ns.contains("cim16"))); + assertTrue("Should contain CIM 100 namespace (CGMES 3.0)", + namespaces.stream().anyMatch(ns -> ns.contains("CIM100"))); + } +} diff --git a/cimxml/testing/application-profiles-library b/cimxml/testing/application-profiles-library new file mode 160000 index 00000000..bd65bab6 --- /dev/null +++ b/cimxml/testing/application-profiles-library @@ -0,0 +1 @@ +Subproject commit bd65bab6e9e80bfa2af73073cc3a6061d2a14399 From f99b79e58226b73d5cc1d6778adaba003935ca87 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 8 Mar 2026 20:01:16 +0000 Subject: [PATCH 4/4] Add ENTSO-E ReliCapGrid as test submodule with grid parsing tests Add the ReliCapGrid synthetic grid model (CC-BY-SA-4.0) as a git submodule for testing CIMXML parsing against real-world CGMES instance data. Includes parameterized tests that load RDFS profiles and parse all grid data files per directory, covering both CGMES 2.4 and CGMES 3.0 grid models. Refactor parser package structure in tests. --- .gitmodules | 3 + README.md | 8 + cimxml/pom.xml | 6 + .../parser/TestConvertCimXmlToJsonLd.java | 3 +- .../parser/TestParserCIMXMLConformity.java | 3 +- .../parser/TestParserRDFXMLConformity.java | 3 +- .../cimxml/parser/TestReliCapGrid.java | 220 ++++++++++++++++++ .../rdfs/TestApplicationProfilesLibrary.java | 4 +- cimxml/testing/relicapgrid | 1 + 9 files changed, 244 insertions(+), 7 deletions(-) rename cimxml/src/test/java/de/soptim/opencgmes/{ => cimxml}/parser/TestConvertCimXmlToJsonLd.java (98%) rename cimxml/src/test/java/de/soptim/opencgmes/{ => cimxml}/parser/TestParserCIMXMLConformity.java (99%) rename cimxml/src/test/java/de/soptim/opencgmes/{ => cimxml}/parser/TestParserRDFXMLConformity.java (98%) create mode 100644 cimxml/src/test/java/de/soptim/opencgmes/cimxml/parser/TestReliCapGrid.java create mode 160000 cimxml/testing/relicapgrid diff --git a/.gitmodules b/.gitmodules index f74786db..7a3ebc06 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,6 @@ [submodule "cimxml/testing/application-profiles-library"] path = cimxml/testing/application-profiles-library url = https://github.com/entsoe/application-profiles-library.git +[submodule "cimxml/testing/relicapgrid"] + path = cimxml/testing/relicapgrid + url = https://github.com/entsoe/relicapgrid.git diff --git a/README.md b/README.md index de4934fe..76ca4901 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,14 @@ This project includes the [ENTSO-E Application Profiles Library](https://github. (v1.1.1) as a Git submodule under `cimxml/testing/application-profiles-library/` for testing purposes only. The Application Profiles Library is licensed under the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). +This project includes the [ENTSO-E ReliCapGrid](https://github.com/entsoe/relicapgrid) +(v1.0.0) as a Git submodule under `cimxml/testing/relicapgrid/` for testing purposes only. +ReliCapGrid is a synthetic grid model containing CGMES instance data for testing CIM data exchange scenarios. +ReliCapGrid is licensed under the +[Creative Commons Attribution-ShareAlike 4.0 International License (CC-BY-SA-4.0)](https://creativecommons.org/licenses/by-sa/4.0/). +Contributors include AspenTech, DIgSILENT, Energinet, gridDigIt, PSE, RTE, Siemens, Statnett, +Svenska kraftnät, Swissgrid, Unicorn, and Valimate. + ## License This project is licensed under the [Apache License 2.0](LICENSE). diff --git a/cimxml/pom.xml b/cimxml/pom.xml index 2ae62b23..99f00c64 100644 --- a/cimxml/pom.xml +++ b/cimxml/pom.xml @@ -26,6 +26,12 @@ Licensed under the Apache License, Version 2.0 https://github.com/entsoe/application-profiles-library --> + diff --git a/cimxml/src/test/java/de/soptim/opencgmes/parser/TestConvertCimXmlToJsonLd.java b/cimxml/src/test/java/de/soptim/opencgmes/cimxml/parser/TestConvertCimXmlToJsonLd.java similarity index 98% rename from cimxml/src/test/java/de/soptim/opencgmes/parser/TestConvertCimXmlToJsonLd.java rename to cimxml/src/test/java/de/soptim/opencgmes/cimxml/parser/TestConvertCimXmlToJsonLd.java index d587219b..3ca9e612 100644 --- a/cimxml/src/test/java/de/soptim/opencgmes/parser/TestConvertCimXmlToJsonLd.java +++ b/cimxml/src/test/java/de/soptim/opencgmes/cimxml/parser/TestConvertCimXmlToJsonLd.java @@ -16,10 +16,9 @@ * limitations under the License. */ -package de.soptim.opencgmes.parser; +package de.soptim.opencgmes.cimxml.parser; import de.soptim.opencgmes.cimxml.graph.CimProfile; -import de.soptim.opencgmes.cimxml.parser.CimXmlParser; import de.soptim.opencgmes.cimxml.sparql.core.LinkedCimDatasetGraph; import java.io.BufferedOutputStream; import java.io.IOException; diff --git a/cimxml/src/test/java/de/soptim/opencgmes/parser/TestParserCIMXMLConformity.java b/cimxml/src/test/java/de/soptim/opencgmes/cimxml/parser/TestParserCIMXMLConformity.java similarity index 99% rename from cimxml/src/test/java/de/soptim/opencgmes/parser/TestParserCIMXMLConformity.java rename to cimxml/src/test/java/de/soptim/opencgmes/cimxml/parser/TestParserCIMXMLConformity.java index 559dcb2a..5897fd90 100644 --- a/cimxml/src/test/java/de/soptim/opencgmes/parser/TestParserCIMXMLConformity.java +++ b/cimxml/src/test/java/de/soptim/opencgmes/cimxml/parser/TestParserCIMXMLConformity.java @@ -16,12 +16,11 @@ * limitations under the License. */ -package de.soptim.opencgmes.parser; +package de.soptim.opencgmes.cimxml.parser; import de.soptim.opencgmes.cimxml.graph.CimProfile; import de.soptim.opencgmes.cimxml.graph.CimProfile17; import de.soptim.opencgmes.cimxml.graph.CimProfile18; -import de.soptim.opencgmes.cimxml.parser.ReaderCIMXML_StAX_SR; import de.soptim.opencgmes.cimxml.parser.system.StreamCimXmlToDatasetGraph; import de.soptim.opencgmes.cimxml.rdfs.CimProfileRegistryStd; import org.apache.jena.datatypes.xsd.XSDDatatype; diff --git a/cimxml/src/test/java/de/soptim/opencgmes/parser/TestParserRDFXMLConformity.java b/cimxml/src/test/java/de/soptim/opencgmes/cimxml/parser/TestParserRDFXMLConformity.java similarity index 98% rename from cimxml/src/test/java/de/soptim/opencgmes/parser/TestParserRDFXMLConformity.java rename to cimxml/src/test/java/de/soptim/opencgmes/cimxml/parser/TestParserRDFXMLConformity.java index d2960fc3..7c24ad36 100644 --- a/cimxml/src/test/java/de/soptim/opencgmes/parser/TestParserRDFXMLConformity.java +++ b/cimxml/src/test/java/de/soptim/opencgmes/cimxml/parser/TestParserRDFXMLConformity.java @@ -16,9 +16,8 @@ * limitations under the License. */ -package de.soptim.opencgmes.parser; +package de.soptim.opencgmes.cimxml.parser; -import de.soptim.opencgmes.cimxml.parser.ReaderCIMXML_StAX_SR; import de.soptim.opencgmes.cimxml.parser.system.StreamCimXmlToDatasetGraph; import org.apache.jena.graph.Graph; import org.apache.jena.riot.RDFParser; diff --git a/cimxml/src/test/java/de/soptim/opencgmes/cimxml/parser/TestReliCapGrid.java b/cimxml/src/test/java/de/soptim/opencgmes/cimxml/parser/TestReliCapGrid.java new file mode 100644 index 00000000..a368253a --- /dev/null +++ b/cimxml/src/test/java/de/soptim/opencgmes/cimxml/parser/TestReliCapGrid.java @@ -0,0 +1,220 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package de.soptim.opencgmes.cimxml.parser; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assume.assumeTrue; + +import de.soptim.opencgmes.cimxml.sparql.core.CimDatasetGraph; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Tests that verify CIMXML grid data files from the ENTSO-E ReliCapGrid can be parsed + * using CIM RDFS profiles from the application-profiles-library. + * + *

      The ReliCapGrid is included as a Git submodule at + * {@code cimxml/testing/relicapgrid} and is licensed under CC-BY-SA-4.0. + * See + * https://github.com/entsoe/relicapgrid

      + * + *

      Each test case corresponds to one directory containing {@code .xml} CIMXML files. + * A {@link CimXmlParser} instance is created per directory with the appropriate RDFS profiles + * loaded, and all {@code .xml} files in that directory are parsed as CIM models.

      + */ +@RunWith(Parameterized.class) +public class TestReliCapGrid { + + private static final Logger LOG = LoggerFactory.getLogger(TestReliCapGrid.class); + + private static final Path RELICAPGRID_ROOT = + Paths.get("testing", "relicapgrid"); + + private static final Path GRID_ROOT = + RELICAPGRID_ROOT.resolve("Instance/Grid"); + + private static final Path PROFILES_ROOT = + Paths.get("testing", "application-profiles-library"); + + private static final Path CGMES_30_RDFS = + PROFILES_ROOT.resolve("CGMES/CurrentRelease/RDFS"); + + private static final Path CGMES_24_RDFS = + PROFILES_ROOT.resolve("CGMES/PastReleases/v2-4/Original/RDFS"); + + private static final Path CGMES_24_GRID_DIR = + GRID_ROOT.resolve("CommonAndBoundaryData/CGMES_2-4"); + + /** + * CGMES 2.4 "Augmented" equipment profiles to skip when loading RDFS, matching the exclusions + * in {@code TestCrossVersionProfileCompatibility#cgmes24And30CoexistInSameRegistry}. + */ + private static final List CGMES_24_SKIPPED_RDFS = List.of( + "EquipmentProfileCoreOperationRDFSAugmented-v2_4_15-4Sep2020.rdf", + "EquipmentProfileCoreRDFSAugmented-v2_4_15-4Sep2020.rdf", + "EquipmentProfileCoreShortCircuitRDFSAugmented-v2_4_15-4Sep2020.rdf" + ); + + private final String testName; + private final Path gridDirectory; + private final boolean isCgmes24; + + public TestReliCapGrid(String testName, Path gridDirectory, boolean isCgmes24) { + this.testName = testName; + this.gridDirectory = gridDirectory; + this.isCgmes24 = isCgmes24; + } + + @Parameterized.Parameters(name = "{0}") + public static Collection discoverGridDirectories() throws IOException { + List testCases = new ArrayList<>(); + + boolean relicapAvailable = Files.exists(RELICAPGRID_ROOT) + && Files.isDirectory(RELICAPGRID_ROOT) + && Files.exists(GRID_ROOT); + boolean profilesAvailable = Files.exists(PROFILES_ROOT) + && Files.isDirectory(PROFILES_ROOT); + + if (!relicapAvailable || !profilesAvailable) { + testCases.add(new Object[]{ + "submodules not available", null, false + }); + return testCases; + } + + // CGMES 2.4 test case + if (Files.exists(CGMES_24_GRID_DIR) && Files.exists(CGMES_24_RDFS)) { + testCases.add(new Object[]{ + "CGMES_2-4/CommonAndBoundaryData", CGMES_24_GRID_DIR, true + }); + } + + // CGMES 3.0 test cases: all directories under Instance/Grid except CGMES_2-4 + try (Stream paths = Files.walk(GRID_ROOT)) { + List xmlDirs = paths + .filter(Files::isRegularFile) + .filter(p -> p.toString().endsWith(".xml")) + .map(Path::getParent) + .distinct() + .filter(dir -> !dir.startsWith(CGMES_24_GRID_DIR)) + .sorted() + .collect(Collectors.toList()); + + for (Path dir : xmlDirs) { + String displayName = GRID_ROOT.relativize(dir).toString(); + testCases.add(new Object[]{displayName, dir, false}); + } + } + + if (testCases.isEmpty()) { + testCases.add(new Object[]{"No grid data found", null, false}); + } + + return testCases; + } + + private CimXmlParser createParserWithProfiles() throws IOException { + CimXmlParser parser = new CimXmlParser(); + + if (isCgmes24) { + loadRdfsProfiles(parser, CGMES_24_RDFS, CGMES_24_SKIPPED_RDFS); + } else { + loadRdfsProfiles(parser, CGMES_30_RDFS, List.of()); + } + + return parser; + } + + private static void loadRdfsProfiles(CimXmlParser parser, Path rdfsDir, + List skippedFiles) throws IOException { + try (Stream paths = Files.list(rdfsDir)) { + List rdfFiles = paths + .filter(Files::isRegularFile) + .filter(p -> p.toString().endsWith(".rdf")) + .filter(p -> !skippedFiles.contains(p.getFileName().toString())) + .sorted() + .collect(Collectors.toList()); + + for (Path rdfFile : rdfFiles) { + String name = rdfFile.getFileName().toString(); + // Skip older RDFS2019 header for CGMES 3.0 (same as cross-version test) + if (name.contains("RDFS2019")) { + continue; + } + try { + parser.parseAndRegisterCimProfile(rdfFile); + } catch (IllegalArgumentException e) { + if (e.getMessage() != null && e.getMessage().contains("already registered")) { + LOG.info("Skipping duplicate profile {}: {}", name, e.getMessage()); + } else { + throw e; + } + } + } + } + } + + private static List listXmlFiles(Path directory) throws IOException { + try (Stream paths = Files.list(directory)) { + return paths + .filter(Files::isRegularFile) + .filter(p -> p.toString().endsWith(".xml")) + .sorted() + .collect(Collectors.toList()); + } + } + + @Test + public void parseAllGridFiles() throws IOException { + assumeTrue("Submodules not available, skipping: " + testName, + gridDirectory != null); + + CimXmlParser parser = createParserWithProfiles(); + List xmlFiles = listXmlFiles(gridDirectory); + assertFalse("No .xml files found in " + testName, xmlFiles.isEmpty()); + + for (Path xmlFile : xmlFiles) { + LOG.info("Parsing grid file: {}", xmlFile.getFileName()); + CimDatasetGraph dataset = parser.parseCimModel(xmlFile); + + assertNotNull("Parsed dataset should not be null for " + xmlFile.getFileName(), + dataset); + assertTrue("Dataset should be a FullModel: " + xmlFile.getFileName(), + dataset.isFullModel()); + assertNotNull("Model header should not be null: " + xmlFile.getFileName(), + dataset.getModelHeader()); + assertTrue("Model body should not be empty: " + xmlFile.getFileName(), + dataset.getBody().size() > 0); + } + } +} diff --git a/cimxml/src/test/java/de/soptim/opencgmes/cimxml/rdfs/TestApplicationProfilesLibrary.java b/cimxml/src/test/java/de/soptim/opencgmes/cimxml/rdfs/TestApplicationProfilesLibrary.java index b7096c4b..a856525a 100644 --- a/cimxml/src/test/java/de/soptim/opencgmes/cimxml/rdfs/TestApplicationProfilesLibrary.java +++ b/cimxml/src/test/java/de/soptim/opencgmes/cimxml/rdfs/TestApplicationProfilesLibrary.java @@ -66,7 +66,9 @@ public class TestApplicationProfilesLibrary { Paths.get("testing", "application-profiles-library"); private static final List EXCLUDED_PATHS = List.of( - "NCP/PastReleases", + // Error: Graphs ontology does not contain the required versionIRI and keyword for a + // CIM profile. + "NCP/PastReleases/v2-3-1/Original/RDFS", "CGMES/CurrentRelease/RDFS/Beta_501_Ed2_CD", // SHACL constraint files (.rdf format) are not RDFS vocabulary profiles "CGMES/CurrentRelease/SHACL", diff --git a/cimxml/testing/relicapgrid b/cimxml/testing/relicapgrid new file mode 160000 index 00000000..f8f12906 --- /dev/null +++ b/cimxml/testing/relicapgrid @@ -0,0 +1 @@ +Subproject commit f8f1290611ef3192996773e49c0dd679b9d7b5ab