001/* 002 * Copyright c 2018 Rusi Popov, MDA Tools.net All rights reserved. 003 * 004 * This program and the accompanying materials are made available under the terms of the 005 * Eclipse Public License v2.0 which accompanies this distribution, and is available at 006 * http://www.eclipse.org/legal/epl-v20.html 007 */ 008package net.mdatools.modelant.core.operation.model; 009 010import java.util.ArrayList; 011import java.util.Collection; 012import java.util.Iterator; 013import java.util.List; 014import java.util.logging.Level; 015import java.util.logging.Logger; 016 017import javax.jmi.reflect.RefObject; 018import javax.jmi.reflect.RefPackage; 019 020import net.mdatools.modelant.core.api.Function; 021import net.mdatools.modelant.core.api.diff.InstanceDifference; 022import net.mdatools.modelant.core.api.diff.ModelComparisonResult; 023import net.mdatools.modelant.core.api.match.ConsideredEqual; 024import net.mdatools.modelant.core.api.match.MatchingCriteria; 025import net.mdatools.modelant.core.operation.element.PrintModelElement; 026import net.mdatools.modelant.core.operation.element.RetrieveAssociations; 027import net.mdatools.modelant.core.operation.element.RetrieveAttributes; 028import net.mdatools.modelant.core.operation.model.topology.CacheClassResults; 029import net.mdatools.modelant.core.operation.model.topology.EquivalenceClassesMap; 030import net.mdatools.modelant.core.operation.model.topology.EquivalenceClassesMapImpl; 031import net.mdatools.modelant.core.operation.model.topology.ModelTopology; 032import net.mdatools.modelant.core.operation.model.topology.Node; 033import net.mdatools.modelant.core.util.Navigator; 034 035/** 036 * Identify structural differences between two models loaded in separate extents in the current repository. They 037 * are treated as different versions of a common model, so one of them is referred as <b>old</b>, whereas the 038 * other is referred as <b>new</b>. The comparison is done according explicitly provided:<ul> 039 * <li> <b>matching criteria</b> stating for each metamodel class what attributes and associations to compare 040 * bound to an instance in the old object and to an instance in the new model, in order to treat both 041 * instances as corresponding. 042 * <li> <b>changeDetectionCriteria</b> relaxing the matching criteria, so that any not matching objects, due to the 043 * matching criteria, but matching due to this one are considered as moved or changed elements. 044 * </ul> 045 * <b>A model elements from the "new" model is treated as equal to a model element from the "old" model, if:</b><ul> 046 * <li>both they have equal (primitive) values of the attributes 047 * <li>AND they refer equal objects through associations 048 * </ul> 049 * Model elements, for which there are no criteria (attributes or associations) specified for their class 050 * are treated as NOT EQUAL. 051 * @author Rusi Popov (popovr@mdatools.net) 052 */ 053public class CompareModels implements Function<RefPackage, ModelComparisonResult> { 054 055 private static final Logger LOGGER = Logger.getLogger( CompareModels.class.getName() ); 056 057 private static final PrintModelElement PRINT_MODEL_ELEMENT = new PrintModelElement(); 058 059 /** 060 * The criteria to use for exactly matching model elements. Thus, elements that match 061 * according to this criteria are treated as equal / same, whereas any other elements are treated 062 * as added or deleted. 063 */ 064 private final MatchingCriteria exactMatchCriteria; 065 066 /** 067 * The attributes and associations to disregard, when identifying the exactly matching model elements. 068 * For example, when matching MOF metamodels, MOF specified the "qualifiedName" derived attribute, which 069 * would have different values for relocaated model elements 070 */ 071 private final MatchingCriteria exceptionMatchCriteria; 072 073 /** 074 * The list of explicit bindings/mappings between elements from and to models 075 */ 076 private final List<ConsideredEqual> bindings = new ArrayList<>(); 077 078 /** 079 * non-null model to compare to as a previous version 080 */ 081 private final RefPackage sourceModelExtent; 082 083 private final Function<RefObject, Collection<String>> retrieveAssociations = new CacheClassResults(new RetrieveAssociations()); 084 085 private final Function<RefObject, Collection<String>> retrieveAttributes = new CacheClassResults(new RetrieveAttributes()); 086 087 088 /** 089 * Use the same exact match and relaxed match criteria, this way treating as not identical the elements, 090 * that are different according to the matching criteria. 091 * 092 * @param exactMatchCriteria not null criteria stating for each metamodel class what attributes and associations 093 * to compare bound to an instance in the old object and to an instance in the new model, in order to treat both 094 * instances as corresponding. Thus, elements that match according to this criteria are treated as equal / same, 095 * whereas any other elements are treated as added or deleted. 096 * @param exceptionMatchCriteria non null criteria which attributes and associations to skip in the identification 097 * of elements differences. 098 * @param bindings non null list defines explicitly listed objects as equals (even though they are not equal in the 099 * sense of exactMatchCriteria). These are model elements that should be treated as a-priori equal. 100 * @param sourceModelExtent not null extent holding the model, treated as "old" 101 * @see #CompareModels(MatchingCriteria, MatchingCriteria, List, RefPackage) 102 */ 103 public CompareModels(MatchingCriteria exactMatchCriteria, 104 MatchingCriteria exceptionMatchCriteria, 105 List<ConsideredEqual> bindings, 106 RefPackage sourceModelExtent) { 107 if ( exactMatchCriteria == null ) { 108 throw new IllegalArgumentException("Expected non-null exact match criteria provided"); 109 } 110 this.exactMatchCriteria = exactMatchCriteria; 111 112 if ( exceptionMatchCriteria == null ) { 113 throw new IllegalArgumentException("Expected non-null exceptions of the match provided"); 114 } 115 this.exceptionMatchCriteria = exceptionMatchCriteria; 116 117 118 if ( bindings == null ) { 119 throw new IllegalArgumentException("Expected non-null list of in advance known equal elements"); 120 } 121 this.bindings.addAll(bindings); 122 123 if ( sourceModelExtent == null ) { 124 throw new IllegalArgumentException("Expected non-null extent of the \"old\" model"); 125 } 126 this.sourceModelExtent = sourceModelExtent; 127 } 128 129 /** 130 * @param targetModelExtent The extent where the new model is loaded 131 * @return a non-null result of comparison of newRootPackage treated as a "new version" of a model 132 * and oldRootPackage, treated as its "old version". The result describes the changes 133 * happened in the old vresion in order to produce the new one. result.getEquals() != null 134 * The ADDED/DELETED model elements are ordered by LEVEL descending, metaclass ascending, 135 * values ascending 136 * @throws IllegalArgumentException 137 */ 138 public ModelComparisonResult execute(RefPackage targetModelExtent) throws IllegalArgumentException { 139 ModelComparisonResult result; 140 EquivalenceClassesMap<RefObject> equals; 141 ModelTopology newTopology; 142 ModelTopology oldTopology; 143 Collection<RefObject> allOldNodes; 144 Collection<RefObject> allNewNodes; 145 List<InstanceDifference> changes; 146 List<InstanceDifference> exactMatches; 147 148 allNewNodes = Navigator.getAllObjects(targetModelExtent); 149 allOldNodes = Navigator.getAllObjects(sourceModelExtent); 150 151 newTopology = new ModelTopology(); 152 newTopology.load(exactMatchCriteria, allNewNodes); 153 154 oldTopology = new ModelTopology(); 155 oldTopology.load(exactMatchCriteria, allOldNodes); 156 157 equals = new EquivalenceClassesMapImpl<>(); 158 159 defineExternallyProvidedEquals( sourceModelExtent, 160 targetModelExtent, 161 bindings, 162 equals); 163 164 excludeEquivalent(oldTopology, newTopology, equals); 165 166 findEquals(oldTopology, newTopology, equals); 167 168 changes = new ArrayList<>(); 169 exactMatches = new ArrayList<>(); 170 171 detectElementChanges(exactMatches, changes, equals); 172 173 result = new ModelComparisonResultImpl(exactMatchCriteria, 174 newTopology.getContents(), // not matched nodes 175 oldTopology.getContents(), // not matched nodes 176 changes, 177 exactMatches); 178 newTopology.clear(); 179 oldTopology.clear(); 180 181 return result; 182 } 183 184 /** 185 * Bind in equivalenceClasses the elements, treated as equal by some extenral criteria 186 * @param sourceRootPackage 187 * @param targetRootPackage 188 * @param consideredEquals 189 * @param equivalenceClasses 190 */ 191 private void defineExternallyProvidedEquals(RefPackage sourceRootPackage, 192 RefPackage targetRootPackage, 193 List<ConsideredEqual> consideredEquals, 194 EquivalenceClassesMap<RefObject> equivalenceClasses) { 195 Collection<RefObject> sourceObjects; 196 Collection<RefObject> targetObjects; 197 198 for (ConsideredEqual binding : consideredEquals) { 199 sourceObjects = binding.selectOld().execute(sourceRootPackage); 200 targetObjects = binding.selectNew().execute(targetRootPackage); 201 202 // map sourceObjects as a class to targetObjects as a class 203 if (!sourceObjects.isEmpty() && !targetObjects.isEmpty()) { 204 equivalenceClasses.add(sourceObjects, targetObjects); 205 } 206 } 207 } 208 209 /** 210 * Exclude from the topologies the elements that are known as equivalent by any external means 211 * @param sourceTopology not null 212 * @param targetTopology not null 213 * @param equivalenceClasses 214 */ 215 private void excludeEquivalent(ModelTopology sourceTopology, 216 ModelTopology targetTopology, 217 EquivalenceClassesMap<RefObject> equivalenceClasses) { 218 219 // exclude from both topologies the already matched (externally) nodes 220 for (RefObject representative : equivalenceClasses.getXKeys()) { 221 sourceTopology.remove( equivalenceClasses.getEquivalents( representative ) ); 222 targetTopology.remove( equivalenceClasses.getEquivalents( equivalenceClasses.map( representative ) ) ); 223 } 224 } 225 226 /** 227 * Identify the equal objects/model elements from this and the provided topology and modifies this 228 * and the provided topologies listing only the not matched elements. This way, this method has 229 * three results - the exact mapping it found and the modified topologies - this and the parameter 230 * one. 231 * POST-CONDITION:<ul> 232 * <li> only model elements that have correspondents are mapped to equivalence classes in knownEquivalences 233 * </ul> 234 * As of https://mdatools.net/mantis/view.php?id=6 (issue 0000006), compared are only nodes of the same 235 * generation of ready nodes. 236 * @param sourceTopology non-null source model topology 237 * @param targetTopology non-null target model topology 238 * @param equivalenceClasses non-null existing/known externally equivalences, 239 * left with the correspondence between model elements form this topology to the 240 * corresponding element form the other topology, including the initial know equivalence 241 * classes. knownEquivalences is modified to be the result. 242 */ 243 private void findEquals(ModelTopology sourceTopology, 244 ModelTopology targetTopology, 245 EquivalenceClassesMap<RefObject> equivalenceClasses) { 246 int iteration; 247 Node<RefObject> sourceRepresentative; 248 Node<RefObject> targetRepresentative; 249 List<Node<RefObject>> matchedSources; 250 List<Node<RefObject>> matchedTargets; 251 252 List<Node<RefObject>> sourceGenerationReady; 253 List<Node<RefObject>> targetGenerationReady; 254 255 List<Node<RefObject>> collectedMatchedSources; 256 List<Node<RefObject>> collectedMatchedTargets; 257 258 List<Node<RefObject>> unmatchedSources; 259 260 unmatchedSources = new ArrayList<>(); 261 262 collectedMatchedSources = new ArrayList<>(); 263 collectedMatchedTargets = new ArrayList<>(); 264 265 iteration = 0; 266 267 sourceGenerationReady = sourceTopology.getGenerationOfReady(); 268 targetGenerationReady = targetTopology.getGenerationOfReady(); 269 270 while ( !sourceGenerationReady.isEmpty() ) { 271 iteration++; 272 273 while ( !sourceGenerationReady.isEmpty() ) { 274 sourceRepresentative = sourceGenerationReady.get(0); 275 276 matchedTargets = Node.findReadyMatches(equivalenceClasses, sourceRepresentative, targetGenerationReady ); 277 278 LOGGER.log( Level.FINER, 279 "Matching {0} found at the target side {1}", 280 new Object[] {PRINT_MODEL_ELEMENT.toPrint(sourceRepresentative), 281 PRINT_MODEL_ELEMENT.toPrint(matchedTargets)} ); 282 283 if ( matchedTargets.isEmpty() ) { // sourceRepresentative was not matched to anything at the other side 284 285 // exclude sourceRepresentative from further matching to guarantee the termination 286 unmatchedSources.add( sourceRepresentative ); 287 sourceGenerationReady.remove( sourceRepresentative ); 288 289 } else { 290 targetRepresentative = matchedTargets.get(0); 291 292 matchedSources = Node.findReadyMatches(equivalenceClasses, targetRepresentative, sourceGenerationReady ); 293 294 LOGGER.log( Level.FINER, 295 "Matching {0} found at the source side {1}", 296 new Object[] {PRINT_MODEL_ELEMENT.toPrint(targetRepresentative), 297 PRINT_MODEL_ELEMENT.toPrint(matchedSources)} ); 298 299 assert matchedSources.contains( sourceRepresentative ) 300 : "Expected this node pertains to its equivalence class"; 301 302 // define the equivalence class matchedAtThisSide mapped to the equivalence class matchedAtOtherSide 303 equivalenceClasses.add( Node.unwrap(matchedSources), 304 Node.unwrap(matchedTargets) ); 305 306 targetGenerationReady.removeAll( matchedTargets ); 307 sourceGenerationReady.removeAll( matchedSources ); 308 309 collectedMatchedSources.addAll( matchedSources ); 310 collectedMatchedTargets.addAll( matchedTargets ); 311 } 312 } // targetGenerationReady contains unmatched target nodes - they could not be matched anymore 313 // sourceGenerationReady is empty, 314 // unmatchedSources contains the unmatched source nodes - they cannot be matched anymore 315 316 LOGGER.log( Level.FINE, 317 "Iteartion {0}, found as deleted: {1}", 318 new Object[] {iteration, PRINT_MODEL_ELEMENT.toPrint( unmatchedSources )} ); 319 LOGGER.log( Level.FINE, 320 "Iteartion {0}, found as added: {1}", 321 new Object[] {iteration, PRINT_MODEL_ELEMENT.toPrint( targetGenerationReady )} ); 322 323 sourceTopology.removeFromReadyNodes( unmatchedSources ); // this and targetTopology contain the next generation ready nodes 324 unmatchedSources.clear(); 325 326 targetTopology.removeFromReadyNodes( targetGenerationReady ); // this and targetTopology contain the next generation ready nodes 327 targetGenerationReady.clear(); 328 329 // produce the next generation in readyNodes 330 sourceTopology.removeFromTopology( collectedMatchedSources ); 331 targetTopology.removeFromTopology( collectedMatchedTargets ); 332 333 collectedMatchedSources.clear(); 334 collectedMatchedTargets.clear(); 335 336 sourceGenerationReady = sourceTopology.getGenerationOfReady(); 337 targetGenerationReady = targetTopology.getGenerationOfReady(); 338 } // any nodes left in this or other topology were not matched or participate in one or more circular dependencies 339 } 340 341 /** 342 * For each model element from the [new] model find the possible elements from the [old] model, 343 * that are equivalent to it, according to the stated criteria, but have changed metamodel attributes 344 * or changed metamodel associations. When there are N model elements in a equivalence class, 345 * according to the criteria, in the new model, mapped to an equivalence class of M model elements 346 * in the old model, then at most NxM pairs of elements, representing a change. 347 * <br/> 348 * For example, in the terms of UML 1.3 these could be [Uml]Class instances that changed their 349 * UML Namespace or name. 350 * Post-condition:<ul> 351 * <ul> result maps elements from one (first) model to correspondent elements from the other (second) model, i.e. 352 * result.get(elemen1) == element2 353 * ==> 354 * correspondents.map(element1) == correspondents.get(element2) 355 * <ul> result does not contain elements that are identically mapped to each other, i.e. 356 * result.get(element1) == element2 357 * ==> 358 * correspondents.getSet(element2) does not contain identically mapped elements for element1 359 * <ul> for each mapped in result element there is no other element mapped that is equal to it 360 * </ul> 361 * @param exactMatches not null list to add the exact matches (not changed elements) detected 362 * @param changes not null list where to add the changed elements detected 363 * @param equivalenceClasses not null 364 */ 365 private void detectElementChanges(List<InstanceDifference> exactMatches, 366 List<InstanceDifference> changes, 367 EquivalenceClassesMap<RefObject> equivalenceClasses) { 368 369 // split each 1:1 mapped classes of equivalent elements into pairs of identical elements into exactMatches 370 equivalenceClasses.getXKeys() 371// The parallel execution causes loops in the buckets in the WeakHashMap used by MDR 372// .parallelStream() 373 .forEach( xRepresentative -> compareRepresentatives(xRepresentative, 374 exactMatches, 375 changes, equivalenceClasses) ); 376 } 377 378 /** 379 * yRepresentative = correspondents.map( xRepresentative ); 380 * @param yRepresentative not null 381 * @param xRepresentative not null 382 * @param exactMatches not null list to add the exact matches (not changed elements) detected 383 * @param changes not null list where to add the changed elements detected 384 * @param equivalenceClasses not null 385 */ 386 private void compareRepresentatives(final RefObject xRepresentative, 387 final List<InstanceDifference> exactMatches, 388 final List<InstanceDifference> changes, 389 EquivalenceClassesMap<RefObject> equivalenceClasses) { 390 RefObject yRepresentative; 391 List<InstanceDifference> xDiffs; 392 InstanceDifferencesImpl exactMatch; 393 InstanceDifferencesImpl diff; 394 Collection<CachedModelElement> yEquivalents; 395 Collection<CachedModelElement> xEquivalents; 396 397 Iterator<CachedModelElement> xEquivalentIterator; 398 Iterator<CachedModelElement> yEquivalentIterator; 399 CachedModelElement xCorrespondent; 400 CachedModelElement yCorrespondent; 401 402 Collection<String> attributeNames; 403 Collection<String> associationNames; 404 405 List<InstanceDifference> detectedChanges; 406 407 attributeNames = retrieveAttributes.execute( xRepresentative ); 408 attributeNames.removeAll( exceptionMatchCriteria.getAttributes( xRepresentative ) ); 409 410 associationNames = retrieveAssociations.execute( xRepresentative ); 411 associationNames.removeAll( exceptionMatchCriteria.getAssociations( xRepresentative ) ); 412 413 yRepresentative = equivalenceClasses.map( xRepresentative ); 414 415 xEquivalents = CachedModelElement.cacheModel(equivalenceClasses.getEquivalents( xRepresentative ), 416 attributeNames, 417 associationNames); 418 yEquivalents = CachedModelElement.cacheModel(equivalenceClasses.getEquivalents( yRepresentative ), 419 attributeNames, 420 associationNames); 421 422 // collect the changes locally, before committing them to the non-local/shared changes list 423 detectedChanges = new ArrayList<>(); 424 425 LOGGER.log( Level.FINE, "Each-to-each comparison of: {0}", new Integer(xEquivalents.size())); 426 427 xEquivalentIterator = xEquivalents.iterator(); 428 while ( xEquivalentIterator.hasNext() ) { 429 xCorrespondent = xEquivalentIterator.next(); 430 431 xDiffs = new ArrayList<>(); 432 exactMatch = null; 433 434 yEquivalentIterator = yEquivalents.iterator(); 435 while ( exactMatch == null && yEquivalentIterator.hasNext() ) { 436 yCorrespondent = yEquivalentIterator.next(); 437 438 diff = new InstanceDifferencesImpl( xCorrespondent, 439 yCorrespondent, 440 equivalenceClasses); 441 if ( diff.isExactMatch() ) { // no diffs of yCorrespondent and xCorrespondent should be reported 442 exactMatch = diff; 443 } else { 444 xDiffs.add( diff ); 445 } 446 } 447 448 if (exactMatch != null) { 449 yEquivalentIterator.remove(); // yMatch excluded from further comparison 450 xEquivalentIterator.remove(); // xMatch excluded from further comparison 451 452 exactMatch.removeCoveredDiffs( detectedChanges ); // no previous comparisons of yMatch 453 xDiffs.clear(); // no previous comparisons of xMatch 454 455 synchronized(exactMatches) { 456 exactMatches.add( exactMatch ); 457 } 458 } else { 459 detectedChanges.addAll( xDiffs ); 460 } 461 } // detectedChanges = {(x,y)| x does not match exactly y}, there may be multiple occurrences of x or y 462 463 synchronized (changes) { 464 changes.addAll( filterOutRepeated( detectedChanges ) ); 465 } 466 } 467 468 469 /** 470 * @param detectedChanges non-null {(x,y)| x does not match exactly y}, there may be multiple occurrences of x or y 471 * @return a non-null list where any node x (or y) occur exactly once 472 */ 473 private List<InstanceDifference> filterOutRepeated(List<InstanceDifference> detectedChanges) { 474 List<InstanceDifference> result; 475 InstanceDifference diff; 476 477 result = new ArrayList<>(); 478 while (!detectedChanges.isEmpty()) { 479 diff = detectedChanges.remove( 0 ); 480 result.add( diff ); 481 482 diff.removeCoveredDiffs( detectedChanges ); 483 } 484 return result; 485 } 486}