diff --git a/pom.xml b/pom.xml
index e95d6798d..52c5217c6 100644
--- a/pom.xml
+++ b/pom.xml
@@ -43,10 +43,36 @@
shapechange-core
- shapechange-ea
shapechange-app
+
+
+
+ ea
+
+
+ !skipEa
+
+
+
+ shapechange-ea
+
+
+
+
+
https://github.com/ShapeChange/ShapeChange
scm:git:https://github.com/ShapeChange/ShapeChange.git
diff --git a/shapechange-app/pom.xml b/shapechange-app/pom.xml
index 9ae55c98d..10d9724cd 100644
--- a/shapechange-app/pom.xml
+++ b/shapechange-app/pom.xml
@@ -14,11 +14,6 @@
shapechange-core
${project.version}
-
- ${project.groupId}
- shapechange-ea
- ${project.version}
-
org.apache.xmlgraphics
fop
@@ -67,17 +62,6 @@
*.properties
-
- ../shapechange-ea/src/main/resources
- true
- ${project.basedir}/sc-resources
-
- **/*
-
-
- *.properties
-
-
@@ -413,4 +397,46 @@
+
+
+
+ ea
+
+
+ !skipEa
+
+
+
+
+ ${project.groupId}
+ shapechange-ea
+ ${project.version}
+
+
+
+
+
+ ../shapechange-ea/src/main/resources
+ true
+ ${project.basedir}/sc-resources
+
+ **/*
+
+
+ *.properties
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/shapechange-app/src/integrationtests/owl/qualifiedCardinalityRestrictions/reference/owl/INPUT/test.ttl b/shapechange-app/src/integrationtests/owl/qualifiedCardinalityRestrictions/reference/owl/INPUT/test.ttl
new file mode 100644
index 000000000..971919e3b
--- /dev/null
+++ b/shapechange-app/src/integrationtests/owl/qualifiedCardinalityRestrictions/reference/owl/INPUT/test.ttl
@@ -0,0 +1,48 @@
+@prefix ex: .
+@prefix owl: .
+@prefix rdf: .
+@prefix rdfs: .
+@prefix xsd: .
+
+ex:Road.width rdf:type owl:DatatypeProperty;
+ rdfs:domain ex:Road;
+ rdfs:range xsd:double .
+
+ex:Road.lane rdf:type owl:ObjectProperty;
+ rdfs:domain ex:Road;
+ rdfs:range ex:Lane .
+
+ex:Road.length rdf:type owl:DatatypeProperty;
+ rdfs:domain ex:Road;
+ rdfs:range xsd:double .
+
+ex:Road rdf:type owl:Class;
+ rdfs:subClassOf [ rdf:type owl:Restriction;
+ owl:onClass ex:Lane;
+ owl:onProperty ex:Road.lane;
+ owl:qualifiedCardinality "1"^^xsd:nonNegativeInteger
+ ];
+ rdfs:subClassOf [ rdf:type owl:Restriction;
+ owl:maxQualifiedCardinality "1"^^xsd:nonNegativeInteger;
+ owl:onDataRange xsd:integer;
+ owl:onProperty ex:Road.speedLimit
+ ];
+ rdfs:subClassOf [ rdf:type owl:Restriction;
+ owl:minQualifiedCardinality "1"^^xsd:nonNegativeInteger;
+ owl:onDataRange xsd:double;
+ owl:onProperty ex:Road.width
+ ];
+ rdfs:subClassOf [ rdf:type owl:Restriction;
+ owl:onDataRange xsd:double;
+ owl:onProperty ex:Road.length;
+ owl:qualifiedCardinality "1"^^xsd:nonNegativeInteger
+ ] .
+
+ex:Lane rdf:type owl:Class .
+
+ex:Road.speedLimit rdf:type owl:DatatypeProperty;
+ rdfs:domain ex:Road;
+ rdfs:range xsd:integer .
+
+
+ rdf:type owl:Ontology .
diff --git a/shapechange-app/src/integrationtests/owl/qualifiedCardinalityRestrictions/testEA_owl_qualifiedCardinalityRestrictions.xml b/shapechange-app/src/integrationtests/owl/qualifiedCardinalityRestrictions/testEA_owl_qualifiedCardinalityRestrictions.xml
new file mode 100644
index 000000000..a616c1165
--- /dev/null
+++ b/shapechange-app/src/integrationtests/owl/qualifiedCardinalityRestrictions/testEA_owl_qualifiedCardinalityRestrictions.xml
@@ -0,0 +1,52 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/shapechange-app/src/integrationtests/owl/qualifiedCardinalityRestrictions/testEA_owl_qualifiedCardinalityRestrictions_runWithSCXML.xml b/shapechange-app/src/integrationtests/owl/qualifiedCardinalityRestrictions/testEA_owl_qualifiedCardinalityRestrictions_runWithSCXML.xml
new file mode 100644
index 000000000..bcfb0a3a0
--- /dev/null
+++ b/shapechange-app/src/integrationtests/owl/qualifiedCardinalityRestrictions/testEA_owl_qualifiedCardinalityRestrictions_runWithSCXML.xml
@@ -0,0 +1,150 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/shapechange-app/src/integrationtests/owl/qualifiedCardinalityRestrictions/test_qualifiedCardinalityRestrictions.qea b/shapechange-app/src/integrationtests/owl/qualifiedCardinalityRestrictions/test_qualifiedCardinalityRestrictions.qea
new file mode 100644
index 000000000..2fb781acf
Binary files /dev/null and b/shapechange-app/src/integrationtests/owl/qualifiedCardinalityRestrictions/test_qualifiedCardinalityRestrictions.qea differ
diff --git a/shapechange-app/src/integrationtests/owl/qualifiedCardinalityRestrictions/test_qualifiedCardinalityRestrictions.zip b/shapechange-app/src/integrationtests/owl/qualifiedCardinalityRestrictions/test_qualifiedCardinalityRestrictions.zip
new file mode 100644
index 000000000..c3d4b3a5e
Binary files /dev/null and b/shapechange-app/src/integrationtests/owl/qualifiedCardinalityRestrictions/test_qualifiedCardinalityRestrictions.zip differ
diff --git a/shapechange-app/src/test/java/de/interactive_instruments/shapechange/app/BasicTest.java b/shapechange-app/src/test/java/de/interactive_instruments/shapechange/app/BasicTest.java
index bc7259724..1586e0e35 100644
--- a/shapechange-app/src/test/java/de/interactive_instruments/shapechange/app/BasicTest.java
+++ b/shapechange-app/src/test/java/de/interactive_instruments/shapechange/app/BasicTest.java
@@ -90,7 +90,6 @@
import de.interactive_instruments.shapechange.core.util.ExternalCallException;
import de.interactive_instruments.shapechange.core.util.ExternalCallUtil;
import de.interactive_instruments.shapechange.core.util.ZipHandler;
-import de.interactive_instruments.shapechange.ea.util.EAModelDiff;
/**
* Basic unit test for ShapeChange
@@ -642,6 +641,9 @@ private void similarYaml(String fileName, String referenceFileName) {
}
}
+ /** EA repository comparison helper, resolved reflectively; see similarEaRepo. */
+ private static final String EA_MODEL_DIFF_CLASSNAME = "de.interactive_instruments.shapechange.ea.util.EAModelDiff";
+
private void similarEaRepo(String fileName, String referenceFileName) {
try {
@@ -656,12 +658,28 @@ private void similarEaRepo(String fileName, String referenceFileName) {
fail("Reference file " + referenceFile.getAbsolutePath() + " does not exist.");
}
- EAModelDiff differ = new EAModelDiff();
+ /*
+ * Loaded by name, exactly as DefaultModelProvider loads EA model readers, so that
+ * these test sources compile and run without the shapechange-ea module on the
+ * classpath. Only .qea comparisons need it; every other assertion in this class is
+ * EA-independent.
+ */
+ Object differ;
+ try {
+ differ = Class.forName(EA_MODEL_DIFF_CLASSNAME).getConstructor().newInstance();
+ } catch (ClassNotFoundException e) {
+ fail("Comparing EA repositories requires the shapechange-ea module, which is not on"
+ + " the classpath. Build with EA support (omit -DskipEa) to run this test."
+ + " Result file: " + fileName);
+ return;
+ }
- boolean similar = differ.similar(file, referenceFile);
+ boolean similar = (Boolean) differ.getClass().getMethod("similar", File.class, File.class)
+ .invoke(differ, file, referenceFile);
+ String diffDetails = (String) differ.getClass().getMethod("getDiffDetails").invoke(differ);
assertTrue(similar, "EA repository output differs from reference result. Result file: " + fileName
- + " - Reference file: " + referenceFileName + ". Details:\n" + differ.getDiffDetails());
+ + " - Reference file: " + referenceFileName + ". Details:\n" + diffDetails);
} catch (Exception e) {
fail("Exception while comparing EA repository '" + fileName + "' to reference file '" + referenceFileName
diff --git a/shapechange-app/src/test/java/de/interactive_instruments/shapechange/app/OntologyTest.java b/shapechange-app/src/test/java/de/interactive_instruments/shapechange/app/OntologyTest.java
index f1232acc5..2c2a6e0ee 100644
--- a/shapechange-app/src/test/java/de/interactive_instruments/shapechange/app/OntologyTest.java
+++ b/shapechange-app/src/test/java/de/interactive_instruments/shapechange/app/OntologyTest.java
@@ -94,4 +94,26 @@ public void testPropertyGeneralizationAndEnrichment() {
new String[] { "ttl" }, "testResults/owl/propertyGeneralizationAndEnrichment/owl",
"src/integrationtests/owl/propertyGeneralizationAndEnrichment/reference/owl");
}
+
+ @Test
+ public void testQualifiedCardinalityRestrictions() {
+ /*
+ * rule-owl-prop-multiplicityAsQualifiedCardinalityRestriction: qualified
+ * cardinality restrictions on datatype properties must use owl:onDataRange
+ * (W3C OWL 2 Structural Specification, Sec. 8.5; Mapping to RDF Graphs,
+ * Sec. 3.2), while restrictions on object properties use owl:onClass
+ * (Sec. 8.3).
+ *
+ * The original model is the Enterprise Architect repository
+ * test_qualifiedCardinalityRestrictions.qea; the SCXML based resources
+ * (test_qualifiedCardinalityRestrictions.zip and the *_runWithSCXML config)
+ * are derived from it via the standard mechanism (run the test with system
+ * variable updateOrCreateScxmlResources=true), so the test executes without
+ * Enterprise Architect on 64bit Java.
+ */
+ multiTest(
+ "src/integrationtests/owl/qualifiedCardinalityRestrictions/testEA_owl_qualifiedCardinalityRestrictions.xml",
+ new String[] { "ttl" }, "testResults/owl/qualifiedCardinalityRestrictions",
+ "src/integrationtests/owl/qualifiedCardinalityRestrictions/reference");
+ }
}
diff --git a/shapechange-core/src/main/java/de/interactive_instruments/shapechange/core/target/ontology/OntologyModel.java b/shapechange-core/src/main/java/de/interactive_instruments/shapechange/core/target/ontology/OntologyModel.java
index 12d5ad84e..591dac880 100644
--- a/shapechange-core/src/main/java/de/interactive_instruments/shapechange/core/target/ontology/OntologyModel.java
+++ b/shapechange-core/src/main/java/de/interactive_instruments/shapechange/core/target/ontology/OntologyModel.java
@@ -1112,6 +1112,25 @@ protected void addUnionSemantics(ClassInfo ci, OntClass c) {
}
}
+ /**
+ * Determines the predicate that links a qualified cardinality restriction to
+ * its restricting range.
+ *
+ * Per the W3C OWL 2 specification, qualified cardinality restrictions on a
+ * data property MUST use owl:onDataRange with a data range (Structural
+ * Specification and Functional-Style Syntax, Sec. 8.5 "Data Property
+ * Cardinality Restrictions"; Mapping to RDF Graphs, Sec. 3.2), whereas
+ * owl:onClass applies only to object property cardinality restrictions
+ * (Sec. 8.3). Emitting owl:onClass with an XSD datatype (e.g. xsd:double)
+ * yields an invalid OWL 2 DL construct.
+ *
+ * @param p the property the restriction applies to
+ * @return OWL2.onDataRange for datatype properties, otherwise OWL2.onClass
+ */
+ protected Property qualifiedRestrictionOnPredicate(OntProperty p) {
+ return (p != null && p.isDatatypeProperty()) ? OWL2.onDataRange : OWL2.onClass;
+ }
+
public OntClass createQCardinalityRestriction(OntProperty p, int cardinality, Resource range) {
/*
* NOTE: We cannot use the create(Min|Max)CardinalityQRestriction(...) methods
@@ -1131,7 +1150,7 @@ public OntClass createQCardinalityRestriction(OntProperty p, int cardinality, Re
Literal cardAsNonNegativeInteger = ontmodel.createTypedLiteral(cardinality,
"http://www.w3.org/2001/XMLSchema#nonNegativeInteger");
restriction.addLiteral(OWL2.qualifiedCardinality, cardAsNonNegativeInteger);
- restriction.addProperty(OWL2.onClass, range);
+ restriction.addProperty(qualifiedRestrictionOnPredicate(p), range);
return restriction;
}
@@ -1155,7 +1174,7 @@ public OntClass createQMinCardinalityRestriction(OntProperty p, int cardinality,
Literal cardAsNonNegativeInteger = ontmodel.createTypedLiteral(cardinality,
"http://www.w3.org/2001/XMLSchema#nonNegativeInteger");
restriction.addLiteral(OWL2.minQualifiedCardinality, cardAsNonNegativeInteger);
- restriction.addProperty(OWL2.onClass, range);
+ restriction.addProperty(qualifiedRestrictionOnPredicate(p), range);
return restriction;
}
@@ -1179,7 +1198,7 @@ public OntClass createQMaxCardinalityRestriction(OntProperty p, int cardinality,
Literal cardAsNonNegativeInteger = ontmodel.createTypedLiteral(cardinality,
"http://www.w3.org/2001/XMLSchema#nonNegativeInteger");
restriction.addLiteral(OWL2.maxQualifiedCardinality, cardAsNonNegativeInteger);
- restriction.addProperty(OWL2.onClass, range);
+ restriction.addProperty(qualifiedRestrictionOnPredicate(p), range);
return restriction;
}