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.util;
009
010import java.util.ArrayList;
011import java.util.Arrays;
012import java.util.Collection;
013import java.util.HashSet;
014import java.util.Iterator;
015import java.util.List;
016
017import javax.jmi.model.Classifier;
018import javax.jmi.model.ModelElement;
019import javax.jmi.reflect.InvalidNameException;
020import javax.jmi.reflect.JmiException;
021import javax.jmi.reflect.RefAssociation;
022import javax.jmi.reflect.RefBaseObject;
023import javax.jmi.reflect.RefClass;
024import javax.jmi.reflect.RefFeatured;
025import javax.jmi.reflect.RefObject;
026import javax.jmi.reflect.RefPackage;
027import javax.jmi.reflect.RefStruct;
028
029import net.mdatools.modelant.core.api.Operation;
030import net.mdatools.modelant.core.api.name.Name;
031import net.mdatools.modelant.core.operation.element.PrintModelElement;
032
033
034/**
035 * Retrieve associated model elements, a single model element, or a single
036 * attribute value, starting from the current model being processed, down an explicit
037 * path provided.
038 * @author Rusi Popov (popovr@mdatools.net)
039 */
040public class Navigator {
041
042  private static final PrintModelElement PRINT_MODEL_ELEMENT = new PrintModelElement();
043
044  /**
045   * Regular expression for allowed separators in object navigation paths
046   */
047  public static final String OBJECT_PATH_SEPARATORS = "\\.";
048
049  /**
050   * The explicit attribute name that refers the MOF ID of the object.
051   */
052  public static final String NAME_MOFID = "MOFID";
053
054  /**
055   * An element of the object navigation path that leads to the obejct's meta-object
056   */
057  public static final String NAME_METAOBJECT = "METAOBJECT";
058
059  /**
060   * An element of the object navigation path that leads to the qualified name of
061   * the obejct's meta-object within the MOF metamodel
062   */
063  public static final String NAME_METACLASSNAME = "METACLASSNAME";
064
065  /**
066   * An element of the object navigation path that leads to the obejct's outer-most package
067   */
068  public static final String NAME_OUTER_MOST_PACKAGE = "OPACKAGE";
069
070  /**
071   * An element of the object navigation path that leads to the obejct's immediate package
072   */
073  public static final String NAME_IMMEDIATE_PACKAGE = "IPACKAGE";
074
075  /**
076   * Collects all values the path describes starting form the startFrom object
077   * <pre>
078   * Format: {asssociationName.}[attributeName]
079   *
080   *  associationName = name
081   *                    | METAOBJECT
082   *                    | METACLASSNAME
083   *                    | IPACKAGE
084   *                    | OPACKAGE
085   *
086   *  attributeName = name
087   *                  | MOFID
088   * where:
089   *   MOFID refers the MOF ID of the object to process
090   *   METAOBJECT retrieves the meta-object of the object to process. This allows reflective navigation.
091   *   METACLASSNAME retrieves the qualified (in the meta-model) name of the meta-class for the processed object.
092   *   IPACKAGE retrieves the immediate package in the meta-model the processed object is in
093   *   OPACKAGE retrieves the outer-most package (the extent) where object processed is in
094   *
095   * </pre>
096   * @param startFrom
097   * @param path is a non-null path following *-to-many or *-to-one associations and ending optionally with attribute
098   * @param reportExceptions in some cases it makes no sense to report the exceptions, while generating them allocates
099   *         significant resources, but the client code does not use them. Set it to true in order to suppress throwing
100   *         the exceptions.
101   * @return a non-null collection of non-null and non-empty string values
102   * @throws JmiException
103   */
104  public static List<? extends Object> collectValues(RefFeatured startFrom,
105                                                     String path,
106                                                     boolean reportExceptions) throws JmiException {
107    ArrayList<Object> result = new ArrayList<Object>();
108    String[] parsedPath;
109
110    parsedPath = path.split(OBJECT_PATH_SEPARATORS);
111
112    collectAllValues( startFrom, parsedPath, 0, result, reportExceptions);
113    return result;
114  }
115
116  private static void collectAllValues(Object startFrom,
117                                       String[] parsedPath,
118                                       int startIndex,
119                                       List<Object> output,
120                                       boolean reportExceptions) {
121    Object result;
122    String itemName;
123    Iterator associatedIterator;
124
125    if ( startIndex >= parsedPath.length ) { // startFrom is the result to store
126      if ( startFrom != null ) {
127        output.add( startFrom );
128      }
129    } else { // there are still associations to go
130      itemName = parsedPath[startIndex];
131      try {
132        result = getReflectiveValue( startFrom, itemName );
133
134      } catch (Exception ex) {
135        if ( reportExceptions ) {
136          throw new IllegalArgumentException("Navigating down the path: " + Arrays.asList(Arrays.copyOf( parsedPath, startIndex-1 ))
137                                           + " reached " + PRINT_MODEL_ELEMENT.execute( startFrom )+" where could not find "+itemName,
138                                           ex);
139        }
140        result = null;
141      }
142
143      if ( result instanceof Collection ) { // this is not an attribute value, so these are associated model elements
144        associatedIterator = ((Collection) result).iterator();
145        while ( associatedIterator.hasNext() ) {
146          collectAllValues( associatedIterator.next(),
147                            parsedPath,
148                            startIndex+1,
149                            output,
150                            reportExceptions);
151        }
152      } else if ( result != null ) {
153        collectAllValues( result,
154                          parsedPath,
155                          startIndex+1,
156                          output,
157                          reportExceptions);
158      }
159    }
160  }
161
162  /**
163   * Retrieve a single value using of the named association or attribute, using the MOF reflective interfaces
164   * @param startFrom
165   * @param itemName
166   * @return the retrieved value
167   * @throws InvalidNameException when the value could not be retrieved
168   */
169  public static Object getReflectiveValue(Object startFrom, String itemName) throws InvalidNameException {
170    Object result;
171
172    if ( NAME_MOFID.equalsIgnoreCase( itemName ) ) {
173      if ( startFrom instanceof RefBaseObject ) {
174        result = ((RefBaseObject) startFrom).refMofId();
175      } else {
176        throw new InvalidNameException( itemName,
177                                        "In order to retrieve the MOF ID of the processed object ("+ NAME_MOFID
178                                        + ") expected an instance of RefBaseObject "
179                                        + " instead of "+ startFrom);
180      }
181    } else if ( NAME_METAOBJECT.equalsIgnoreCase( itemName ) ) {
182      if ( startFrom instanceof RefBaseObject) {
183        result = ((RefBaseObject) startFrom).refMetaObject();
184      } else {
185        throw new InvalidNameException( itemName,
186                                        "In order to retrieve the meta-object of the processed object ("+ NAME_METAOBJECT
187                                        + ") expected an instance of RefBaseObject "
188                                        + " instead of "+ startFrom);
189      }
190    } else if ( NAME_METACLASSNAME.equalsIgnoreCase( itemName ) ) {
191      if ( startFrom instanceof RefObject  ) {
192        result = constructQualifiedName((ModelElement) ((RefObject) startFrom).refClass().refMetaObject());
193
194      } else {
195        throw new InvalidNameException( itemName,
196                                        "In order to retrieve the metaclass of the processed object ("+ NAME_METACLASSNAME
197                                        + ") expected an instance of RefObject "
198                                        + " instead of "+ startFrom);
199      }
200    } else if ( NAME_OUTER_MOST_PACKAGE.equalsIgnoreCase( itemName )) {
201      if ( startFrom instanceof RefBaseObject) {
202        result = ((RefBaseObject) startFrom).refOutermostPackage();
203      } else {
204        throw new InvalidNameException( itemName,
205                                        "In order to retrieve the outer-most package in the meta-model the processed object is in"+ NAME_OUTER_MOST_PACKAGE
206                                        + " expected an instance of RefBaseObject "
207                                        + " instead of "+ startFrom);
208      }
209    } else if ( NAME_IMMEDIATE_PACKAGE.equalsIgnoreCase( itemName )) {
210      if ( startFrom instanceof RefBaseObject) {
211        result = ((RefBaseObject) startFrom).refImmediatePackage();
212      } else {
213        throw new InvalidNameException( itemName,
214                                        "In order to retrieve the immediate package in the meta-model the processed object is in ("+ NAME_IMMEDIATE_PACKAGE
215                                        + ") expected an instance of RefBaseObject "
216                                        + " instead of "+ startFrom);
217      }
218    } else if ( startFrom instanceof RefFeatured ) {
219      try {
220        result = ((RefFeatured) startFrom).refGetValue( itemName );
221      } catch (JmiException ex) {
222        throw new IllegalArgumentException(" Retrieving the value of field '"+itemName+"'"
223//                                        + " of "+ PRINT_MODEL_ELEMENT.execute( startFrom )
224                                        + " caused ", ex);
225      }
226    } else if ( startFrom instanceof RefStruct ) {
227        try {
228          result = ((RefStruct) startFrom).refGetValue( itemName );
229        } catch (JmiException ex) {
230          throw new IllegalArgumentException(" Retrieving the value of field '"+itemName+"'"
231//                                          + " of "+ PRINT_MODEL_ELEMENT.execute( startFrom )
232                                            + " caused ", ex);
233        }
234    } else {
235      throw new InvalidNameException( itemName,
236                                      "Expected a RefFeatured or RefStruct instance instead of "
237                                      +startFrom
238                                      +" to get its "+itemName );
239    }
240    return result;
241  }
242
243
244  /**
245   * This method locates all objects in the model package/extent
246   * @param sourceExtent non-null extent where to find the metaclass
247   * @return an iterator on all objects in meta-classes in this or nested packages
248   */
249  public static List<RefObject> getAllObjects(RefPackage sourceExtent ) {
250    ArrayList<RefObject> result;
251
252    result = new ArrayList<RefObject>();
253
254
255    process(sourceExtent,
256        new ProcessPackage() {
257          public RefPackage execute(RefPackage refPackage) throws RuntimeException, IllegalArgumentException {
258            for (RefClass metaClass : (Collection<RefClass>) refPackage.refAllClasses()) {
259              result.addAll( metaClass.refAllOfClass() );
260            }
261            return null;
262          }
263        });
264    return result;
265  }
266
267  /**
268   * @param sourceExtent non-null extent where to find the metaclasses
269   * @return non-null list of all meta-classes in sourceExtent and its sub-packages
270   */
271  public static List<RefAssociation> getAllAssociations(RefPackage sourceExtent ) {
272    ArrayList<RefAssociation> result;
273
274    result = new ArrayList<>();
275
276    process(sourceExtent,
277            new ProcessPackage() {
278              public RefPackage execute(RefPackage refPackage) throws RuntimeException, IllegalArgumentException {
279                result.addAll( refPackage.refAllAssociations() );
280                return null;
281              }
282            });
283    return result;
284  }
285
286  /**
287   * This method locates all objects in the model package/extent
288   * @param sourceExtent non-null extent where to find the metaclass
289   * @return an iterator on all objects in meta-classes in this or nested packages
290   */
291  public static List<RefClass> getAllClasses(RefPackage sourceExtent ) {
292    ArrayList<RefClass> result;
293
294    result = new ArrayList<>();
295
296    process(sourceExtent,
297            new ProcessPackage() {
298              public RefPackage execute(RefPackage refPackage) throws RuntimeException, IllegalArgumentException {
299                result.addAll( refPackage.refAllClasses() );
300                return null;
301              }
302            });
303    return result;
304  }
305
306  /**
307   * Process the provided package in a specific way.
308   * The operation's result is not used for further processing.
309   */
310  private interface ProcessPackage extends Operation<RefPackage> {
311  }
312
313  /**
314   * This method adds all model objects found in the meta classes in the package provided
315   * or in its subpackages
316   * @param thisPackage is the meta-package to collect objects in
317   * @param result is a non-null list of all model objects
318   */
319  private static void process(RefPackage thisPackage, ProcessPackage processPackage) {
320    processPackage.execute(thisPackage);
321
322    for (RefPackage nested : (Collection<RefPackage>) thisPackage.refAllPackages()) {
323      process( nested, processPackage);
324    }
325  }
326
327
328  /**
329   * This method finds the metapackage with the name provided
330   *
331   * @param rootPackage is the top-most package / the model's extent
332   * @param metaPackageName a meta package name with syntax [&lt;package&gt;{::&lt;package&gt;}]
333   * @return the non-null meta package
334   * @throws JmiException if the meta package name is not a valid one
335   */
336  public static RefPackage getMetaPackage(RefPackage rootPackage, String metaPackageName) throws JmiException {
337    RefPackage result;
338    String[] parsedPath;
339    String itemName;
340
341    assert rootPackage != null : "Expected a non-null package";
342
343    result = rootPackage;
344
345    // parse syntax [<package>{::<package>}]
346    parsedPath = Name.parseQualifiedName( metaPackageName );
347
348    for (int i = 0; i < parsedPath.length; i++) {
349      itemName = parsedPath[i];
350
351      try {
352        result = result.refPackage( itemName );
353      } catch (JmiException ex) {
354        throw new IllegalArgumentException("Looking up the package "+ metaPackageName
355                                        + " down the path: " + Arrays.asList( Arrays.copyOf( parsedPath, i ) )
356                                        + " reached " + PRINT_MODEL_ELEMENT.execute( result )
357                                        + " for which retrieving the nested package '"+itemName+"'"
358                                        + " caused ", ex);
359      }
360    }
361    return result;
362  }
363
364  /**
365   * This method parses the metaclass parameter, matches it with the meta model structure, and finds
366   * the meta class with the qualified metaclass name.
367   *
368   * @param rootPackage is the top-most package / the model's extent
369   * @param metaClassName a meta class qualified name with syntax {&lt;package&gt;::} &lt;class&gt;
370   * @return the non-null metaclass described in the <code> metaclass </code> attribute
371   * @throws JmiException if the metaclass name is not a valid one
372   */
373  public static RefClass getMetaClass(RefPackage rootPackage, String metaClassName) throws JmiException {
374    RefClass result = null;
375    int i;
376    String[] parsedPath;
377    String itemName;
378
379    assert rootPackage != null : "Expected a non-null package";
380
381    // parse syntax {<package>::}<class>
382    parsedPath = Name.parseQualifiedName( metaClassName );
383    i = 0;
384    while ( i < parsedPath.length ) {
385      itemName = parsedPath[i++];
386
387      try {
388        if ( i < parsedPath.length ) { // this is a package name
389          rootPackage = rootPackage.refPackage( itemName );
390
391        } else { // this is a meta class name, the parsing process will terminate
392          result = rootPackage.refClass( itemName );
393        }
394      } catch (JmiException ex) {
395        throw new IllegalArgumentException("Looking up the package "+ metaClassName
396                                        + " down the path: " + Arrays.asList( Arrays.copyOf( parsedPath, i-1 ) )
397                                        + " reached " + PRINT_MODEL_ELEMENT.execute( result )
398                                        + " for which retrieving the nested package '"+itemName+"'"
399                                        + " caused ", ex);
400      }
401    }
402    return result;
403  }
404
405  /**
406   * This method constructs the name of the metaclass of the model element provided.
407   * @param element is the non-null object to find metaclass of
408   * @return the non-null metaclass name
409   * @throws JmiException if the metaclass name is not a valid one
410   */
411  public static String getMetaClassName(RefObject element) throws JmiException {
412    assert element != null : "Expected a non-null model element";
413
414    return constructQualifiedName((ModelElement) element.refMetaObject());
415  }
416
417  /**
418   * @param metaObject this is a MOF object
419   * @return the qualified name of the MOF element, calculated down the containment relation
420   */
421  private static String constructQualifiedName(ModelElement metaObject) {
422    StringBuilder result = new StringBuilder(256);
423
424    insertQualifiedName( metaObject, result );
425
426    return result.toString();
427  }
428
429  /**
430   * Construct in result the qualified name of the metaObject
431   * @param metaObject not null
432   * @param result not null
433   */
434  private static void insertQualifiedName(ModelElement metaObject, StringBuilder result) {
435    ModelElement next;
436
437    next = metaObject.getContainer();
438    if ( next != null ) {
439      insertQualifiedName( next, result );
440      result.append(Name.METAMODEL_PATH_SEPARATOR);
441    }
442    result.append( metaObject.getName() );
443  }
444
445  /**
446   * This method retrieves the descriptions of all super classes in the metamodel
447   * @param mofMetaObject
448   * @return a non-null unique collection of superclasses of the provided class from the metamodel
449   */
450  public static Collection<Classifier> getAllSuperMetaObejcts(Classifier mofMetaObject) {
451    Collection result;
452    List<Classifier> toProcess;
453
454    result = new HashSet(11);
455    toProcess = new ArrayList<Classifier>();
456    toProcess.add( mofMetaObject );
457
458    while ( !toProcess.isEmpty() ) {
459      mofMetaObject = toProcess.remove( 0 );
460
461      if ( result.add( mofMetaObject ) ) {
462         toProcess.addAll( mofMetaObject.allSupertypes() );
463      }
464    }
465    return result;
466  }
467}