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}