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.topology;
009
010import java.util.ArrayList;
011import java.util.Collection;
012import java.util.Collections;
013import java.util.HashMap;
014import java.util.Iterator;
015import java.util.List;
016import java.util.Map;
017import java.util.Set;
018import java.util.logging.Level;
019import java.util.logging.Logger;
020
021import javax.jmi.reflect.RefBaseObject;
022import javax.jmi.reflect.RefObject;
023import javax.jmi.reflect.RefStruct;
024
025import net.mdatools.modelant.core.api.match.MatchingCriteria;
026import net.mdatools.modelant.core.operation.element.PrintElementRestricted;
027import net.mdatools.modelant.core.operation.element.PrintModelElement;
028import net.mdatools.modelant.core.util.Navigator;
029import net.mdatools.modelant.core.util.key.Hash;
030
031/**
032 * This class represents a single model element with its associated other elements and attributes
033 * among the attributes and associations defined in the owner Topology.
034 * <b>
035 * In a separate step each node should be assigned the nodes that represent the associated model
036 * elements among those that are contained in the topology.
037 * </b>
038 * @param <V>
039 * @see Node#assignAssociatedNodes(Map)
040 */
041public class Node<V extends RefObject> {
042
043  /**
044   * This is a common logger
045   */
046  private static Logger LOGGER = Logger.getLogger( Node.class.getPackage().getName() );
047
048
049  private static final PrintModelElement PRINT_MODEL_ELEMENT = new PrintModelElement();
050
051  /**
052   * The wrapped model element this node is for
053   */
054  private final V wrapped;
055
056  /**
057   * The attribute values to compare,
058   * Contains the names of all attributes of the model element, as defined in the mdetamodel,
059   * mapped to their actual values. null values are allowed.
060   * Used for nodes matching
061   */
062  private final Map<String, List<String>> attributeValues = new HashMap<>(3);
063  private final int attributesHash;
064
065  /**
066   * The model elements that are associated to the wrapped model element.
067   * referredModelElements.get(key) == wrapped.refGet(key) (as a list)
068   * Used for nodes matching
069   */
070  private final Map<String, Collection<V>> referredModelElements = new HashMap<>(3);
071
072  /**
073   * The effective list of Nodes that reference this
074   */
075  private final List<Node<V>> referers = new ArrayList<>();
076
077  /**
078   * The common criteria to compare model elements (and print the wrapped model element)
079   */
080  private final MatchingCriteria criteria;
081
082  /**
083   * The number of model elements still not matched to correspondents in the other model.
084   * Invariant:
085   *   non-negative
086   *   level == count(referredModelElements.values().values()\{processed nodes})
087   */
088  private int level;
089
090  /**
091   * Stores the current key when calculating it is not possible
092   */
093  private Key key;
094
095  /**
096   * Represents the wrapped model element as a Node with its associations and attributes
097   * @param wrapped is not-null
098   * @param criteria
099   */
100  public Node(V wrapped, MatchingCriteria criteria) {
101    Iterator<String> attributesIterator;
102    String attribute;
103    List<?> values;
104    List<String> printableValues;
105    String stringValue;
106    int valuesHash;
107
108    this.wrapped = wrapped;
109    this.criteria = criteria;
110    this.level = 0;
111
112    valuesHash = 0;
113
114    attributesIterator = criteria.getAttributes(wrapped).iterator();
115    while ( attributesIterator.hasNext() ) {
116      attribute = attributesIterator.next();
117
118      valuesHash <<= 1;
119      try {
120        values = Navigator.collectValues( wrapped,
121                                          attribute,
122                                          LOGGER.isLoggable( Level.FINE ));
123
124        // convert all structures, enum, etc. values into printable & comparable ones, independent of their the order
125        printableValues = new ArrayList<>();
126        for ( Object value: values ) {
127          if ( value instanceof RefBaseObject || value instanceof RefStruct ) {
128            stringValue = PRINT_MODEL_ELEMENT.execute( value );
129
130            valuesHash += Hash.hash( stringValue );
131            printableValues.add(stringValue);
132
133          } else if ( value != null ) {
134            stringValue = value.toString();
135
136            valuesHash += Hash.hash( stringValue );
137            printableValues.add(stringValue);
138          }
139        }
140        attributeValues.put(attribute, printableValues );
141      } catch (Exception ex) {
142        LOGGER.log( Level.FINE,
143                    "Model element: {0} does not support attribute: {1}",
144                    new Object[]{PRINT_MODEL_ELEMENT.execute(getWrapped()), attribute});
145      }
146    }
147    this.attributesHash = valuesHash;
148
149    assignKey();
150  }
151
152  private void assignKey() {
153    key = new Key(getWrapped(), getLevel(), attributesHash);
154  }
155
156  /**
157   * This method assigns the Nodes that represent the model elements associated to the wrapped one
158   * and sets in level the number of referenced objects.
159   * @param modelToNodeMap non-null
160   */
161  public void assignAssociatedNodes(Map<V, Node<V>> modelToNodeMap) {
162    Iterator<String> associationsIterator;
163    String association;
164    Collection<V> associated;
165    Node<V> associatedNode;
166
167    associationsIterator = criteria.getAssociations(getWrapped()).iterator();
168    while ( associationsIterator.hasNext() ) {
169      association = associationsIterator.next();
170
171      try {
172        associated = (Collection<V>) Navigator.collectValues( wrapped,
173                                                              association,
174                                                              LOGGER.isLoggable( Level.FINE ));
175
176        for (V associatedModelElement: associated ) {
177          associatedNode = modelToNodeMap.get( associatedModelElement );
178
179          assert associatedNode != null
180                : "Expected a node bound for object "+PRINT_MODEL_ELEMENT.execute(associatedModelElement);
181
182          level++;
183
184          associatedNode.referers.add(this);
185        }
186        referredModelElements.put( association, associated );
187      } catch (Exception ex) {
188        LOGGER.log( Level.FINE,
189                    "Model element: {0} does not support association: {1}",
190                    new Object[]{PRINT_MODEL_ELEMENT.execute(getWrapped()), association});
191      }
192    }
193  }
194
195  /**
196   * Find all ready nodes in this topology, that are equal to the provided node (from the other topology)
197   * @param equivalenceClasses not null collected so far matches/classes of equivalence
198   * @param nodeToMatch not null node to match
199   * @param generationReady TODO
200   * @return non-null list of all ready nodes in this topology, that match the provided node. The provided
201   *         nodes are not mapped in the equivalence classes map, as they are found among the ready nodes,
202   *         mapped in equivalence classes map only when they are reported in this result.
203   */
204  public static <V extends RefObject> List<Node<V>> findReadyMatches(EquivalenceClassesMap<V> equivalenceClasses,
205                                                                     Node<V> nodeToMatch,
206                                                                     List<Node<V>> generationReady) {
207    List<Node<V>> result;
208
209    result = new ArrayList<Node<V>>();
210
211    assert nodeToMatch.isReady() : "Expected a ready node " + nodeToMatch;
212
213    for (Node<V> target : generationReady) {
214      if ( nodeToMatch.getKey().equals(target.getKey())
215           && nodeToMatch.matches( target, equivalenceClasses ) ) {
216        result.add( target );
217      }
218    }
219    return result;
220  }
221
222  /**
223   * Two nodes match if and only if their attribute values are equal and they are associated to
224   * already matched nodes.
225   * Treat the nodes with no attributes and associations as not comparable (different).
226   * PRE-CONDITION:
227   *   getLevel() == 0
228   *   otherNode.getLeve() == 0
229   *
230   * @param otherNode is a non-null node whose wrapped element is of the same metaclass as of this one
231   * @param equivalenceClasses holds the already matched pairs, which classes never are replaced or deleted (i.e. only new classes may be added)
232   * @return true if this matches to the other node
233   */
234  private boolean matches(Node otherNode, EquivalenceClassesMap<V> equivalenceClasses) {
235    boolean result;
236    Map.Entry<String, List<String>> entry;
237    Iterator<Map.Entry<String, List<String>>> attributeEntriesIterator;
238    Iterator<String> associationNamesIterator;
239    String thisAssociationName;
240    Object thisAttributeValue;
241    Object otherAttributeValue;
242    Set<V> theseMappedRepersentatives;
243    Set<V> otherRepersentatives;
244
245    assert getLevel() == 0 : "Expeceted only level 0 objects are matched from, instead of "+getLevel();
246    assert otherNode.getLevel() == 0 : "Expeceted only level 0 objects are matched to, instead of "+otherNode.getLevel();
247
248    result = this.attributesHash == otherNode.attributesHash;
249
250    // compare attributes
251    attributeEntriesIterator = attributeValues.entrySet().iterator();
252    while ( result && attributeEntriesIterator.hasNext() ) {
253      entry = attributeEntriesIterator.next();
254
255      thisAttributeValue = entry.getValue(); // not null
256      otherAttributeValue= otherNode.attributeValues.get( entry.getKey() );
257
258      result = thisAttributeValue.equals( otherAttributeValue );
259    }
260
261    // compare associations - make sure that the nodes refer the same matched objects
262    associationNamesIterator = referredModelElements.keySet().iterator();
263    while ( result && associationNamesIterator.hasNext() ) {
264      thisAssociationName = associationNamesIterator.next();
265
266      theseMappedRepersentatives= equivalenceClasses.getMappedRepresentatives( this.referredModelElements( thisAssociationName ));
267      otherRepersentatives = equivalenceClasses.getRepresentatives( otherNode.referredModelElements( thisAssociationName ));
268
269      result = theseMappedRepersentatives.equals( otherRepersentatives );
270    }
271    return result;
272  }
273
274  /**
275   * @param associationName non-null association name
276   * @return non-null set of model elements, this wrapped element refers in the association with the provided name
277   */
278  private Collection<V> referredModelElements(String associationName) {
279    Collection<V> result;
280
281    result = referredModelElements.get( associationName );
282    if (result == null) {
283      result = Collections.EMPTY_SET;
284    }
285    return result;
286  }
287
288  /**
289   * Decreases the number of references this
290   */
291  public void decreaseLevel() {
292    level--;
293    assert level >= 0 : "Expected a non-negative number of objects this refers to";
294
295    assignKey();
296  }
297
298  /**
299   * @return the list of all nodes
300   */
301  public final List<Node<V>> getReferers() {
302    return referers;
303  }
304
305
306  private int getLevel() {
307    return level;
308  }
309
310  public final boolean isReady() {
311    return level == 0;
312  }
313
314  public final V getWrapped() {
315    return wrapped;
316  }
317
318  final Key getKey() {
319    return key;
320  }
321
322  public String toString() {
323    StringBuilder result = new StringBuilder();
324
325    result.append("Node ")
326          .append(level)
327          .append(" ")
328          .append(new PrintElementRestricted("  ",criteria).execute(wrapped));
329    return result.toString();
330  }
331
332  /**
333   * Convert the list of nodes to a list of their wrapped objects
334   * @param nodes not null
335   * @return not null list of the wrapped objects in the elements of the nodes
336   */
337  public static <V extends RefObject> Collection<V> unwrap(List<Node<V>> nodes) {
338    Collection<V> result;
339
340    result = new ArrayList<>();
341    for (Node<V> node : nodes) {
342      result.add( node.getWrapped() );
343    }
344    return result;
345  }
346}