Skip to content

Commit 62d4901

Browse files
committed
Basic implementation of the cycle detector.
1 parent 646aa93 commit 62d4901

File tree

1 file changed

+162
-0
lines changed
  • core/sds-aspect-model-validator/src/main/java/io/openmanufacturing/sds/aspectmodel/validation/services

1 file changed

+162
-0
lines changed
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
/*
2+
* Copyright (c) 2022 Robert Bosch Manufacturing Solutions GmbH
3+
*
4+
* See the AUTHORS file(s) distributed with this work for additional
5+
* information regarding authorship.
6+
*
7+
* This Source Code Form is subject to the terms of the Mozilla Public
8+
* License, v. 2.0. If a copy of the MPL was not distributed with this
9+
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
10+
*
11+
* SPDX-License-Identifier: MPL-2.0
12+
*/
13+
14+
package io.openmanufacturing.sds.aspectmodel.validation.services;
15+
16+
import java.util.ArrayList;
17+
import java.util.HashSet;
18+
import java.util.List;
19+
import java.util.Optional;
20+
import java.util.Set;
21+
22+
import org.antlr.v4.runtime.misc.OrderedHashSet;
23+
import org.apache.jena.query.Query;
24+
import org.apache.jena.query.QueryExecution;
25+
import org.apache.jena.query.QueryExecutionFactory;
26+
import org.apache.jena.query.QueryFactory;
27+
import org.apache.jena.query.QuerySolution;
28+
import org.apache.jena.query.ResultSet;
29+
import org.apache.jena.rdf.model.Model;
30+
import org.apache.jena.rdf.model.Property;
31+
import org.apache.jena.rdf.model.Resource;
32+
import org.apache.jena.rdf.model.Statement;
33+
import org.apache.jena.rdf.model.StmtIterator;
34+
import org.apache.jena.sparql.core.Var;
35+
import org.apache.jena.sparql.engine.binding.Binding;
36+
import org.apache.jena.vocabulary.RDF;
37+
38+
import io.openmanufacturing.sds.aspectmetamodel.KnownVersion;
39+
import io.openmanufacturing.sds.aspectmodel.resolver.services.VersionedModel;
40+
import io.openmanufacturing.sds.aspectmodel.validation.report.ValidationError;
41+
import io.openmanufacturing.sds.aspectmodel.validation.report.ValidationReport;
42+
import io.openmanufacturing.sds.aspectmodel.validation.report.ValidationReportBuilder;
43+
import io.openmanufacturing.sds.aspectmodel.vocabulary.BAMM;
44+
45+
/**
46+
* Cycle detector for the models.
47+
*
48+
* Because of the limitations of the property paths in Sparql queries, it is impossible to realize the cycle detection together with
49+
* other validations via Shacl shapes.
50+
*
51+
* According to graph theory:
52+
* A directed graph G is acyclic if and only if a depth-first search of G yields no back edges.
53+
* So a depth-first traversal of the "resolved" property references (via complex types like Entities) is able to deliver us all cycles present in the model.
54+
*/
55+
public class ModelCycleDetector {
56+
57+
private final static String prefixes = "prefix bamm: <urn:bamm:io.openmanufacturing:meta-model:%s#> \r\n" +
58+
"prefix bamm-c: <urn:bamm:io.openmanufacturing:characteristic:%s#> \r\n" +
59+
"prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> \r\n";
60+
61+
final OrderedHashSet<String> discovered = new OrderedHashSet<>();
62+
final Set<String> finished = new HashSet<>();
63+
64+
private Query query;
65+
private Property bammOptional;
66+
private Property bammProperty;
67+
68+
List<ValidationError.Semantic> cycleReports = new ArrayList<>();
69+
70+
public ValidationReport validateModel( final VersionedModel versionedModel ) {
71+
discovered.clear();
72+
finished.clear();
73+
cycleReports.clear();
74+
75+
final Model model = versionedModel.getModel();
76+
final Optional<KnownVersion> metaModelVersion = KnownVersion.fromVersionString( versionedModel.getVersion().toString() );
77+
final BAMM bamm = new BAMM( metaModelVersion.get() );
78+
bammProperty = bamm.property();
79+
bammOptional = bamm.optional();
80+
initializeQuery( metaModelVersion.get() );
81+
82+
final StmtIterator properties = model.listStatements( null, RDF.type, bamm.Property() );
83+
while ( properties.hasNext() ) {
84+
final Statement property = properties.nextStatement();
85+
final String fullPropertyName = property.getSubject().getURI();
86+
if ( !discovered.contains( fullPropertyName ) && !finished.contains( fullPropertyName ) ) {
87+
depthFirstTraversal( model, property.getSubject() );
88+
}
89+
}
90+
91+
return cycleReports.isEmpty() ?
92+
new ValidationReport.ValidReport() :
93+
new ValidationReportBuilder().withValidationErrors( cycleReports ).buildInvalidReport();
94+
}
95+
96+
private void depthFirstTraversal( final Model model, final Resource currentProperty ) {
97+
final String currentPropertyName = currentProperty.getURI();
98+
discovered.add( currentPropertyName );
99+
100+
// if (either) -> continue with fake cycleReports for both branches and only add the cycle if both branches have cycles
101+
// reachableObject.getObject == bammEither
102+
//
103+
// else normal handling of properties
104+
105+
final List<Resource> nextHopProperties = getDirectlyReachableProperties( model, currentProperty );
106+
for ( Resource reachableProperty : nextHopProperties ) {
107+
108+
if ( reachableProperty.isAnon() ) { // property usage of the type "[ bamm:property :propName ; bamm:optional true ; ]"
109+
final Statement optional = reachableProperty.getProperty( bammOptional );
110+
if ( (null != optional) && optional.getBoolean() ) {
111+
// presence of bamm:optional = true; no need to continue on this path, the potential cycle would be broken by the optional property anyway
112+
continue;
113+
}
114+
// resolve the property reference
115+
reachableProperty = reachableProperty.getProperty( bammProperty ).getObject().asResource();
116+
}
117+
118+
final String reachablePropertyName = reachableProperty.getURI();
119+
120+
if ( discovered.contains( reachablePropertyName ) ) {
121+
// cycle detected
122+
reportCycle();
123+
} else if ( !finished.contains( reachablePropertyName ) ) {
124+
depthFirstTraversal( model, reachableProperty );
125+
}
126+
}
127+
128+
discovered.remove( discovered.size() - 1 ); // OrderedHashSet does not implement remove( Object )
129+
finished.add( currentPropertyName );
130+
}
131+
132+
private void reportCycle() {
133+
final String cycledNodes = String.join( " -> ", discovered );
134+
cycleReports.add( new ValidationError.Semantic(
135+
String.format(
136+
"The Aspect Model contains a cycle which includes following properties: %s. Please remove any cycles that do not allow a finite json payload.",
137+
cycledNodes ),
138+
"", "", "ERROR", "" ) );
139+
}
140+
141+
private void initializeQuery( final KnownVersion metaModelVersion ) {
142+
final String currentVersionPrefixes = String.format( prefixes, metaModelVersion.toVersionString(), metaModelVersion.toVersionString() );
143+
final String queryString = String.format(
144+
"%s select ?reachableProperty " +
145+
"where { ?currentProperty bamm:characteristic/bamm-c:baseCharacteristic*/bamm-c:left*/bamm-c:right*/bamm:dataType/bamm:properties/rdf:rest*/rdf:first ?reachableProperty }",
146+
currentVersionPrefixes );
147+
query = QueryFactory.create( queryString );
148+
}
149+
150+
private List<Resource> getDirectlyReachableProperties( final Model model, final Resource currentProperty ) {
151+
final List<Resource> reachableProperties = new ArrayList<>();
152+
try ( final QueryExecution qexec = QueryExecutionFactory.create( query, model ) ) {
153+
qexec.setInitialBinding( Binding.builder().add( Var.alloc( "currentProperty" ), currentProperty.asNode() ).build() );
154+
final ResultSet results = qexec.execSelect();
155+
while ( results.hasNext() ) {
156+
final QuerySolution solution = results.nextSolution();
157+
reachableProperties.add( solution.getResource( "reachableProperty" ) );
158+
}
159+
}
160+
return reachableProperties;
161+
}
162+
}

0 commit comments

Comments
 (0)