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/.gitmodules b/.gitmodules new file mode 100644 index 00000000..7a3ebc06 --- /dev/null +++ b/.gitmodules @@ -0,0 +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/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 00000000..a514b6e8 --- /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 +/vcs.xml +/*.iml 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/README.md b/README.md index 8d48ab75..76ca4901 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,20 @@ 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). + +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/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..99f00c64 100644 --- a/cimxml/pom.xml +++ b/cimxml/pom.xml @@ -20,6 +20,18 @@ Copyright © 2004 World Wide Web Consortium Used under W3C Software License for testing purposes only --> + + @@ -27,7 +39,7 @@ de.soptim.opencgmes cimxml - 1.0.0-SNAPSHOT + 1.1.0-SNAPSHOT jar OpenCGMES - IEC61970-552 CIMXML @@ -59,31 +71,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 +207,11 @@ ${ver.plugin.compiler} ${java.version} + + -Xlint:all,-options + -Werror + -proc:none + @@ -225,6 +252,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..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 @@ -18,9 +18,16 @@ 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.TypeMapper; import org.apache.jena.datatypes.xsd.XSDDatatype; import org.apache.jena.datatypes.xsd.impl.RDFLangString; import org.apache.jena.graph.Graph; @@ -31,288 +38,315 @@ 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 cims: + PREFIX rdf: + PREFIX rdfs: + PREFIX xsd: + + SELECT ?rdfType ?property ?cimDatatype ?primitiveType ?referenceType + WHERE + { + ?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; + 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"); + 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, + rdfDataType, + rdfDataType != null ? 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/parser/TestConvertCimXmlToJsonLd.java b/cimxml/src/test/java/de/soptim/opencgmes/cimxml/parser/TestConvertCimXmlToJsonLd.java new file mode 100644 index 00000000..3ca9e612 --- /dev/null +++ b/cimxml/src/test/java/de/soptim/opencgmes/cimxml/parser/TestConvertCimXmlToJsonLd.java @@ -0,0 +1,146 @@ +/* + * 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 de.soptim.opencgmes.cimxml.graph.CimProfile; +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; + +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/cimxml/parser/TestParserCIMXMLConformity.java similarity index 88% 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 e5b5cc0b..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,16 +16,17 @@ * limitations under the License. */ -package de.soptim.opencgmes.parser; +package de.soptim.opencgmes.cimxml.parser; -import de.soptim.opencgmes.cimxml.CimVersion; 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 de.soptim.opencgmes.cimxml.graph.CimProfile17; +import de.soptim.opencgmes.cimxml.graph.CimProfile18; +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 +35,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 +48,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 +67,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 +125,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 +206,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 +226,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 +250,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 +299,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 +342,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 +378,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 +507,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 +523,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/cimxml/parser/TestParserRDFXMLConformity.java similarity index 93% 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 80711d1e..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,16 +16,14 @@ * 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 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 +147,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 +163,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) 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/CimProfileRegistryStdTest.java b/cimxml/src/test/java/de/soptim/opencgmes/cimxml/rdfs/CimProfileRegistryStdTest.java deleted file mode 100644 index e0ebf5c4..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)); - assertNull(registry.getPropertiesAndDatatypes(owlVersionIRIs)); - } - } - - @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)); - assertNull(registry.getPropertiesAndDatatypes(owlVersionIRIs)); - } - } - -} \ No newline at end of file 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..a856525a --- /dev/null +++ b/cimxml/src/test/java/de/soptim/opencgmes/cimxml/rdfs/TestApplicationProfilesLibrary.java @@ -0,0 +1,250 @@ +/* + * 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( + // 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", + // 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/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/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/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 151c085f..00000000 --- a/cimxml/src/test/java/de/soptim/opencgmes/cimxml/sparql/core/CimDatasetGraphTest.java +++ /dev/null @@ -1,226 +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.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().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().parseCimModel(new StringReader(rdfXmlFullModel)); - var differenceModel = new CimXmlParser().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/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 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