|
| 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