40
40
import org .eclipse .esmf .aspectmodel .resolver .services .VersionedModel ;
41
41
import org .eclipse .esmf .aspectmodel .shacl .violation .ProcessingViolation ;
42
42
import org .eclipse .esmf .aspectmodel .shacl .violation .Violation ;
43
-
44
- import org .eclipse .esmf .samm .KnownVersion ;
45
-
46
43
import org .eclipse .esmf .aspectmodel .vocabulary .SAMM ;
44
+ import org .eclipse .esmf .samm .KnownVersion ;
47
45
48
46
/**
49
47
* Cycle detector for SAMM models.
@@ -61,11 +59,14 @@ public class ModelCycleDetector {
61
59
static String ERR_CYCLE_DETECTED =
62
60
"The Aspect Model contains a cycle which includes following properties: %s. Please remove any cycles that do not allow a finite json payload." ;
63
61
64
- private final static String prefixes = "prefix samm: <urn:samm:org.eclipse.esmf.samm:meta-model:%s#> \r \n " +
65
- "prefix samm-c: <urn:samm:org.eclipse.esmf.samm:characteristic:%s#> \r \n " +
66
- "prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> \r \n " ;
62
+ private final static String prefixes = """
63
+ prefix samm: <urn:samm:org.eclipse.esmf.samm:meta-model:%s#>
64
+ prefix samm-c: <urn:samm:org.eclipse.esmf.samm:characteristic:%s#>
65
+ prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
66
+ """ ;
67
67
68
68
final Set <String > discovered = new LinkedHashSet <>();
69
+ final Set <String > discoveredOptionals = new HashSet <>();
69
70
final Set <String > finished = new HashSet <>();
70
71
71
72
private Query query ;
@@ -77,6 +78,7 @@ public class ModelCycleDetector {
77
78
78
79
public List <Violation > validateModel ( final VersionedModel versionedModel ) {
79
80
discovered .clear ();
81
+ discoveredOptionals .clear ();
80
82
finished .clear ();
81
83
cycleDetectionReport .clear ();
82
84
@@ -94,19 +96,7 @@ public List<Violation> validateModel( final VersionedModel versionedModel ) {
94
96
final Iterator <RDFNode > aspectProperties = properties .getList ().iterator ();
95
97
while ( aspectProperties .hasNext () ) {
96
98
final RDFNode propRef = aspectProperties .next ();
97
- final Resource property ;
98
- if ( propRef .isAnon () ) {
99
- if ( isOptionalProperty ( propRef .asResource () ) ) {
100
- continue ;
101
- }
102
- property = resolvePropertyReference ( propRef .asResource () );
103
- } else {
104
- property = propRef .asResource ();
105
- }
106
- final String propertyName = getUniqueName ( property );
107
- if ( !discovered .contains ( propertyName ) && !finished .contains ( propertyName ) ) {
108
- depthFirstTraversal ( property , this ::reportCycle );
109
- }
99
+ depthFirstTraversal ( propRef .asResource (), this ::reportCycle );
110
100
}
111
101
}
112
102
}
@@ -115,56 +105,76 @@ public List<Violation> validateModel( final VersionedModel versionedModel ) {
115
105
}
116
106
117
107
private void depthFirstTraversal ( final Resource currentProperty , final BiConsumer <String , Set <String >> cycleHandler ) {
118
- final String currentPropertyName = getUniqueName ( currentProperty );
119
- discovered .add ( currentPropertyName );
120
-
121
- final List <NextHopProperty > nextHopProperties = getDirectlyReachableProperties ( model , currentProperty );
122
-
123
- // samm-c:Either makes the task somewhat more complicated - we need to know the status of both branches (left/right)
124
- // to be able to decide whether there really is a cycle or not
125
- if ( reachedViaEither ( nextHopProperties ) ) {
126
- final EitherCycleDetector leftBranch = new EitherCycleDetector ( currentPropertyName , this ::reportCycle );
127
- final EitherCycleDetector rightBranch = new EitherCycleDetector ( currentPropertyName , this ::reportCycle );
128
- nextHopProperties .stream ().filter ( property -> property .eitherStatus == 1 )
129
- .forEach ( property -> investigateProperty ( property .propertyNode , leftBranch ::collectCycles ) );
130
- nextHopProperties .stream ().filter ( property -> property .eitherStatus == 2 )
131
- .forEach ( property -> investigateProperty ( property .propertyNode , rightBranch ::collectCycles ) );
132
- if ( leftBranch .hasBreakableCycles () && rightBranch .hasBreakableCycles () ) {
133
- // the cycles found are breakable, but they are present in both branches, resulting in an overall unbreakable cycle
134
- leftBranch .reportCycles ( this ::reportCycle );
135
- rightBranch .reportCycles ( this ::reportCycle );
108
+ final Resource resolvedProperty = currentProperty .isAnon () ? resolvePropertyReference ( currentProperty .asResource () ) : currentProperty .asResource ();
109
+ final String currentPropertyName = getUniqueName ( resolvedProperty );
110
+ if ( finished .contains ( currentPropertyName ) ) {
111
+ return ;
112
+ }
113
+ final boolean isOptional = isOptionalProperty ( currentProperty );
114
+ if ( isOptional ) {
115
+ discoveredOptionals .add ( currentPropertyName );
116
+ }
117
+
118
+ if ( discovered .contains ( currentPropertyName ) ) {
119
+ // found a back edge -> cycle detected; report it as such only if not broken by an optional property
120
+ if ( !optionalPropertyAtOrBelowBackEdge ( currentPropertyName ) ) {
121
+ cycleHandler .accept ( currentPropertyName , discovered );
122
+ }
123
+ } else {
124
+ discovered .add ( currentPropertyName );
125
+
126
+ final List <NextHopProperty > nextHopProperties = getDirectlyReachableProperties ( model , resolvedProperty );
127
+
128
+ // samm-c:Either makes the task somewhat more complicated - we need to know the status of both branches (left/right)
129
+ // to be able to decide whether there really is a cycle or not
130
+ if ( reachedViaEither ( nextHopProperties ) ) {
131
+ final EitherCycleDetector leftBranch = new EitherCycleDetector ( currentPropertyName , this ::reportCycle );
132
+ final EitherCycleDetector rightBranch = new EitherCycleDetector ( currentPropertyName , this ::reportCycle );
133
+ nextHopProperties .stream ().filter ( property -> property .eitherStatus == 1 )
134
+ .forEach ( property -> depthFirstTraversal ( property .propertyNode , leftBranch ::collectCycles ) );
135
+ nextHopProperties .stream ().filter ( property -> property .eitherStatus == 2 )
136
+ .forEach ( property -> depthFirstTraversal ( property .propertyNode , rightBranch ::collectCycles ) );
137
+ if ( leftBranch .hasBreakableCycles () && rightBranch .hasBreakableCycles () ) {
138
+ // the cycles found are breakable, but they are present in both branches, resulting in an overall unbreakable cycle
139
+ leftBranch .reportCycles ( this ::reportCycle );
140
+ rightBranch .reportCycles ( this ::reportCycle );
141
+ }
142
+ } else { // "normal" path
143
+ nextHopProperties .forEach ( property -> depthFirstTraversal ( property .propertyNode , cycleHandler ) );
136
144
}
137
- } else { // "normal" path
138
- nextHopProperties .forEach ( property -> investigateProperty ( property .propertyNode , cycleHandler ) );
145
+
146
+ discovered .remove ( currentPropertyName );
147
+ finished .add ( currentPropertyName );
139
148
}
140
149
141
- discovered .remove ( currentPropertyName );
142
- finished .add ( currentPropertyName );
150
+ if ( isOptional ) {
151
+ discoveredOptionals .remove ( currentPropertyName );
152
+ }
143
153
}
144
154
145
155
private boolean reachedViaEither ( final List <NextHopProperty > nextHopProperties ) {
146
156
return nextHopProperties .stream ().anyMatch ( property -> property .eitherStatus > 0 );
147
157
}
148
158
149
- private void investigateProperty ( Resource propertyNode , final BiConsumer <String , Set <String >> cycleHandler ) {
150
- if ( propertyNode .isAnon () ) {
151
- // [ samm:property :propName ; samm:optional value ; ]
152
- if ( isOptionalProperty ( propertyNode ) ) {
153
- // presence of samm:optional = true; no need to continue on this path, the potential cycle would be broken by the optional property anyway
154
- return ;
159
+ private boolean optionalPropertyAtOrBelowBackEdge ( final String backEdge ) {
160
+ if ( discoveredOptionals .contains ( backEdge ) ) {
161
+ return true ;
162
+ }
163
+ final Iterator <String > path = discovered .iterator ();
164
+ // first find the back edge property within the current path
165
+ while ( path .hasNext () ) {
166
+ final String currentNode = path .next ();
167
+ if ( currentNode .equals ( backEdge ) ) {
168
+ break ;
155
169
}
156
-
157
- propertyNode = resolvePropertyReference ( propertyNode );
158
170
}
159
-
160
- final String propertyName = getUniqueName ( propertyNode );
161
-
162
- if ( discovered .contains ( propertyName ) ) {
163
- // found a back edge -> cycle detected
164
- cycleHandler .accept ( propertyName , discovered );
165
- } else if ( !finished .contains ( propertyName ) ) {
166
- depthFirstTraversal ( propertyNode , cycleHandler );
171
+ // look for an optional property at or below the back edge property
172
+ while ( path .hasNext () ) {
173
+ if ( discoveredOptionals .contains ( path .next () ) ) {
174
+ return true ;
175
+ }
167
176
}
177
+ return false ;
168
178
}
169
179
170
180
private Resource resolvePropertyReference ( final Resource propertyNode ) {
@@ -212,24 +222,25 @@ private void reportCycle( final String cyclePath ) {
212
222
213
223
private void initializeQuery ( final KnownVersion metaModelVersion ) {
214
224
final String currentVersionPrefixes = String .format ( prefixes , metaModelVersion .toVersionString (), metaModelVersion .toVersionString () );
215
- final String queryString = String .format (
216
- "%s select ?reachableProperty ?viaEither"
217
- + " where {"
218
- + " {"
219
- + " ?currentProperty samm:characteristic/samm-c:baseCharacteristic*/samm-c:left/samm:dataType/samm:properties/rdf:rest*/rdf:first ?reachableProperty"
220
- + " bind (1 as ?viaEither)"
221
- + " }"
222
- + " union"
223
- + " {"
224
- + " ?currentProperty samm:characteristic/samm-c:baseCharacteristic*/samm-c:right/samm:dataType/samm:properties/rdf:rest*/rdf:first ?reachableProperty"
225
- + " bind (2 as ?viaEither)"
226
- + " }"
227
- + " union"
228
- + " {"
229
- + " ?currentProperty samm:characteristic/samm-c:baseCharacteristic*/samm:dataType/samm:properties/rdf:rest*/rdf:first ?reachableProperty"
230
- + " bind (0 as ?viaEither)"
231
- + " }"
232
- + "}" ,
225
+ final String queryString = String .format ( """
226
+ %s select ?reachableProperty ?viaEither
227
+ where {
228
+ {
229
+ ?currentProperty samm:characteristic/samm-c:baseCharacteristic*/samm-c:left/samm:dataType/samm:properties/rdf:rest*/rdf:first ?reachableProperty
230
+ bind (1 as ?viaEither)
231
+ }
232
+ union
233
+ {
234
+ ?currentProperty samm:characteristic/samm-c:baseCharacteristic*/samm-c:right/samm:dataType/samm:properties/rdf:rest*/rdf:first ?reachableProperty
235
+ bind (2 as ?viaEither)
236
+ }
237
+ union
238
+ {
239
+ ?currentProperty samm:characteristic/samm-c:baseCharacteristic*/samm:dataType/samm:properties/rdf:rest*/rdf:first ?reachableProperty
240
+ bind (0 as ?viaEither)
241
+ }
242
+ }
243
+ """ ,
233
244
currentVersionPrefixes );
234
245
query = QueryFactory .create ( queryString );
235
246
}
0 commit comments