package com.openMap1.mapper.actions; import java.util.Hashtable; import java.util.Iterator; import org.eclipse.emf.ecore.EAnnotation; import org.eclipse.emf.ecore.EAttribute; import org.eclipse.emf.ecore.EClass; import org.eclipse.emf.ecore.EClassifier; import org.eclipse.emf.ecore.EPackage; import org.eclipse.emf.ecore.EReference; import org.eclipse.emf.ecore.EStructuralFeature; import org.eclipse.jface.action.Action; import org.eclipse.jface.action.IAction; import com.openMap1.mapper.core.MapperException; import com.openMap1.mapper.presentation.MapperEditor; import com.openMap1.mapper.util.FileUtil; import com.openMap1.mapper.views.ClassModelView; import com.openMap1.mapper.views.FeatureView; import com.openMap1.mapper.views.LabelledEClass; import com.openMap1.mapper.views.WorkBenchUtil; /** * this action imports message simplification annotations from one Ecore model to another. * The importing Ecore model is the one attached to the current mapping set. * The Ecore model imported from is selected by a file dialogue. * * Imports are grouped by top-level RMIMs immediately below the root class, * which are packages in the Ecore models; * so the importing model and the model imported from must share some RMIMs. * While importing annotations for one RMIM, existing annotations are removed from the RMIM in the importing * Ecore model. * * Recursion down the tree of RMIM classes (LabelledEClass objects) is cut off before repeat of any class * name. This will affect simplification of recursive models or recursive data types. * It could be altered to allow some maximum depth of nesting of the same class name. * * @author Robert * */ public class ImportSimplificationsAction extends Action implements IAction{ private MapperEditor mapperEditor; private ClassModelView classModelView; private String importedAnnotationURI; /** the package which is the root of the model */ private EPackage ecoreRoot; /** * @param theClass * @return true or false, to cut off recursion at 2 nested occurrences of a class name */ private boolean continueRecursion(LabelledEClass theClass) {return (theClass.nameOccurrences() < MAX_CLASS_OCCURRENCES);} private int MAX_CLASS_OCCURRENCES = 2; // another cutoff for recursion, as the first seems not to work private int MAX_DEPTH = 20; /* records those classes in the model being imported from, which have a different package * from the root class and have some simplification annotations in their sub-tree */ private Hashtable<String,LabelledEClass> packageChangeRootClasses; // keys to annotations are paths to LabelledEClasses with these prefixes static String[] pathPrefixes = {"fixed:",""}; boolean tracing = true; public ImportSimplificationsAction() { super("Import Simplifications from another Ecore Model"); } public void run() { classModelView = WorkBenchUtil.getClassModelView(false); if (classModelView != null) try { // check the importing Ecore model is an RMIM model (LabelledEClass tree) ecoreRoot = classModelView.ecoreRoot(); if (!ClassModelView.isRMIMRoot(ecoreRoot)) throw new MapperException("Importing class model is not an RMIM class model"); LabelledEClass importingRoot = ClassModelView.getRootLabelledEClass(ecoreRoot); // user selects the Ecore model to import simplifications from String mappingSetURIString = classModelView.mappingSetURI().toString(); mapperEditor = WorkBenchUtil.getMapperEditor(mappingSetURIString); if (mapperEditor != null) { String[] exts = {"*.ecore"}; String path = FileUtil.getFilePathFromUser(mapperEditor,exts,"Select Ecore Model",false); if (path.equals("")) return; EPackage importedEcoreRoot = FileUtil.getClassModel(path); if (!ClassModelView.isRMIMRoot(importedEcoreRoot)) throw new MapperException("Imported class model is not an RMIM class model"); LabelledEClass importedRoot = ClassModelView.getRootLabelledEClass(importedEcoreRoot); // find the URI used for imported annotations (there must be exactly 1) importedAnnotationURI = getITSAnnotationURI(importedEcoreRoot); if (importedAnnotationURI == null) throw new MapperException("Selected class model has no simplifications to import"); // do the real work of importing importSimplificationsFromRoot(importedRoot, importingRoot); // say how many were imported; the count is wrong and has been removed String message = "Simplification annotations have been imported."; WorkBenchUtil.showMessage("Simplifications imported",message); // save the resulting Ecore model with imports FileUtil.saveResource(ecoreRoot.eResource()); } } catch (Exception ex) { WorkBenchUtil.showMessage("Error",ex.getMessage()); ex.printStackTrace(); } } /** * does the real work of importing simplification annotations * @param importedRoot root class of the RMIM Ecore model being imported from * @param importingRoot root class of the importing model * @throws MapperException */ private int importSimplificationsFromRoot(LabelledEClass importedRoot, LabelledEClass importingRoot) throws MapperException { // set up a table of classes which change package from the root class package, and have simplifications to import under them packageChangeRootClasses = new Hashtable<String,LabelledEClass>(); String topImportedPackageName = importedRoot.eClass().getEPackage().getName(); int depth = 0; findPackageRootImportedClasses(importedRoot, topImportedPackageName,depth); /* descent of the importing model, looking for package changes. * if the package-changing class matches one with simplifications in its subtree in the imported model, * copy across the simplifications */ String topImportingPackageName = importingRoot.eClass().getEPackage().getName(); findPackageChangingImportingClasses(importingRoot, topImportingPackageName); // import simplifications in the top package int simplifications = importSimplifications(importedRoot,importingRoot,true,topImportingPackageName,importedAnnotationURI); return simplifications; } /** * recursively add to a table of classes which change package from the root, and have simplifications to import under them * @param importedClass current node of the LabelledEClass tree * @param topPackageName name of top package * @throws MapperException */ private void findPackageRootImportedClasses(LabelledEClass importedClass, String topPackageName,int depth) throws MapperException { String importedPackageName = importedClass.eClass().getEPackage().getName(); // for any change of package name, stop the recursion; and note if there are simplifications in the subtree if (!importedPackageName.equals(topPackageName)) { // simplifications in a data type class directly under the top package will not be imported if ((!importedPackageName.equals("datatypes")) && (hasSimplificationInSubtree(importedClass,importedAnnotationURI))) { if (!importedClass.isSingleChild()) throw new MapperException("Package changing class at path '" + importedClass.getPath() + "' is not under a 1..1 association"); packageChangeRootClasses.put(importedClass.getPath(), importedClass); } return; } // search the subtree for (Iterator<LabelledEClass> it = importedClass.getChildren().iterator(); it.hasNext();) { LabelledEClass childClass = it.next(); if ((continueRecursion(childClass)) && (depth < MAX_DEPTH)) { trace("Importing depth " + depth + " at class " + childClass.getQualifiedClassName()); findPackageRootImportedClasses(childClass,topPackageName,depth + 1); } } } /** * Recursive scan of importing classes, looking for package changes * and if so doing any necessary imports of simplification annotations beneath the parent of * the package-changing class. * The parent is typically an ActRelationship or a Participation * @param importingClass * @param topPackageName * @throws MapperException */ private void findPackageChangingImportingClasses(LabelledEClass importingClass, String topPackageName) throws MapperException { String packageName = importingClass.eClass().getEPackage().getName(); // for any change of package name, stop the recursion; and import any simplifications in the subtree if (!packageName.equals(topPackageName)) { LabelledEClass importedClass = packageChangeRootClasses.get(importingClass.getPath()); if ((importedClass != null) && (packageName.equals(importedClass.eClass().getEPackage().getName()))) { // mark as used all the associations from the root down to the parent of the importing class importingClass.parent().markWithAncestors(true); // mark the subtree below the parent of the importing class, following the markings of the imported class subtree importSubtreeSimplifications(importedClass.parent(),importingClass.parent()); // rename the association to the parent of the importing class renameAssociationTo(importedClass.parent(),importingClass.parent()); } return; } // search the subtree for (Iterator<LabelledEClass> it = importingClass.getChildren().iterator(); it.hasNext();) { LabelledEClass childClass = it.next(); if (continueRecursion(childClass)) findPackageChangingImportingClasses(childClass,topPackageName); } } /** * The trees under importingClass and importedClass should match exactly. * Simplification annotations from the imported class subtree are copied * across to the importing class subtree. * Existing simplifications in the importing class subtree are removed. * @param importedClass * @param importingClass * @throws MapperException if the subtrees do not match */ private int importSubtreeSimplifications(LabelledEClass importedClass,LabelledEClass importingClass) throws MapperException { // import simplification annotations for each EAttribute or EReference of this class // in importing simplifications, require an exact match of features on this EClass; it comes from the same template MIF int simplifications = importClassSimplifications(importedClass,importingClass, true,importedAnnotationURI); // recurse over child LabelledEClasses which have annotations beneath them for (Iterator<LabelledEClass> ic = importingClass.getChildren().iterator(); ic.hasNext();) { LabelledEClass child = ic.next(); String childName = child.eClass().getName(); String assocName = child.associationName(); LabelledEClass importedChild = importedClass.getNamedAssocAndClassChild(assocName,childName); if ((importedChild != null) &&(hasSimplificationInSubtree(importedChild,importedAnnotationURI))) simplifications = simplifications + importSubtreeSimplifications(importedChild, child); } return simplifications; } /** * import all simplification annotations in the top package; stop the recursion * at any class which changes packages (to a package other than the data types package), * or which is not matched exactly in the imported model, * or which does not have annotations beneath it * * @param importingClass * @param importedClass * @param topImportingPackageName * @return the number of simplification annotations imported * @throws MapperException */ public static int importSimplifications(LabelledEClass importedClass,LabelledEClass importingClass, boolean stopAtPackageChange, String topImportingPackageName, String annotationURI) throws MapperException { int simplifications = 0; // (possibly) stop recursion at any class which changes package, except to the data types package String packageName = importingClass.eClass().getEPackage().getName(); if ((stopAtPackageChange) && (!packageName.equals(topImportingPackageName)) && (!packageName.equals("datatypes"))) return 0; // import simplification annotations for each EAttribute or EReference of this class // in importing simplifications, do not require an exact match of features on this EClass simplifications = simplifications + importClassSimplifications(importedClass,importingClass, false,annotationURI); // recurse over child LabelledEClasses which are matched and when the matches have annotations beneath them for (Iterator<LabelledEClass> ic = importingClass.getChildren().iterator(); ic.hasNext();) { LabelledEClass child = ic.next(); LabelledEClass importedChild = importedClass.getNamedAssocAndClassChild(child.associationName(),child.eClass().getName()); if ((importedChild != null) && (hasSimplificationInSubtree(importedChild,annotationURI))) { simplifications = simplifications + importSimplifications(importedChild,child, stopAtPackageChange,topImportingPackageName,annotationURI); } } return simplifications; } /** * Import the simplification annotations on the EAttributes and EReferences of one LabelledEClass. * @param fromClass * @param toClass * @param mustMatchExactly if true, just write a warning for any mismatch * @param annotationURI * @return the number of simplification annotations imported * @throws MapperException */ public static int importClassSimplifications(LabelledEClass fromClass,LabelledEClass toClass, boolean mustMatchExactly, String annotationURI) throws MapperException { int simplifications = 0; String className = fromClass.eClass().getName(); // import simplification annotations for each EAttribute or EReference of this class for (Iterator<EStructuralFeature> is = toClass.eClass().getEStructuralFeatures().iterator(); is.hasNext();) { EStructuralFeature toFeature = is.next(); String featureName = toFeature.getName(); EStructuralFeature fromFeature = fromClass.eClass().getEStructuralFeature(featureName); if ((fromFeature == null) && (mustMatchExactly)) System.out.println("Feature " + featureName + " is missing but expected in imported class " + className); if (fromFeature != null) { // remove existing annotations EAnnotation existingNote = toFeature.getEAnnotation(FeatureView.microITSURI()); if (existingNote != null) { for (int p = 0; p > pathPrefixes.length;p++) { String prefixedToPath = pathPrefixes[p] + toClass.getPath(); String value = existingNote.getDetails().get(prefixedToPath); if (value != null) { // remove this key of the annotation existingNote.getDetails().removeKey(prefixedToPath); // if the annotation has no other keys, remove it altogether if (existingNote.getDetails().size() == 0) { toFeature.getEAnnotations().remove(existingNote); } } } } // import annotations EAnnotation fromNote = fromFeature.getEAnnotation(annotationURI); if (fromNote != null) { for (int p = 0; p < pathPrefixes.length;p++) { String prefixedFromPath = pathPrefixes[p] + fromClass.getPath(); String prefixedToPath = pathPrefixes[p] + toClass.getPath(); String value = fromNote.getDetails().get(prefixedFromPath); if (value != null) { FeatureView.addMicroITSAnnotation(toFeature, prefixedToPath, value); simplifications++; } } } } } return simplifications; } /** * @param importedClass * @param importingClass * The association to the imported class from its parent has been renamed. Apply the same renaming annotation * to the association to the importing class from its parent */ private void renameAssociationTo(LabelledEClass importedClass,LabelledEClass importingClass) { // find the associations to the classes, in the imported and importing class models LabelledEClass importedParent = importedClass.parent(); LabelledEClass importingParent = importingClass.parent(); EReference importedRef = (EReference)importedParent.eClass().getEStructuralFeature(importedClass.associationName()); EReference importingRef = (EReference)importingParent.eClass().getEStructuralFeature(importingClass.associationName()); // import the annotation EAnnotation note = importedRef.getEAnnotation(importedAnnotationURI); if (note != null) { String value = note.getDetails().get(importedParent.getPath()); if (value != null) FeatureView.addMicroITSAnnotation(importingRef, importingParent.getPath(), value); } } /** * search an Ecore model for any micro-ITS annotations, and return the URI they use. * Throw an exception if they use more than one URI. * @param model * @return */ public static String getITSAnnotationURI(EPackage model) throws MapperException { String annotationURI = null; // iterate over all packages under the top package for (Iterator<EPackage> ip = model.getESubpackages().iterator();ip.hasNext();) { EPackage subPackage = ip.next(); // Iterate over all EClasses in the subpackage for (Iterator<EClassifier> ic = subPackage.getEClassifiers().iterator();ic.hasNext();) { EClassifier ecl = ic.next(); if (ecl instanceof EClass) { // iterate over all EAttributes and EReferences of the class for (Iterator<EStructuralFeature> is = ((EClass)ecl).getEStructuralFeatures().iterator();is.hasNext();) { EStructuralFeature feature = is.next(); // iterate over all microITS EAnnotations of the structural feature for (Iterator<EAnnotation> ia = feature.getEAnnotations().iterator();ia.hasNext();) { EAnnotation note = ia.next(); if (note.getSource().startsWith(FeatureView.microITSURIStart)) { String nextURI = note.getSource(); if (annotationURI != null) { if (!nextURI.equals(annotationURI)) throw new MapperException("Ecore model has more than one simplification mapping set '" + nextURI + "' and '" + annotationURI + "'"); } else annotationURI = nextURI; } } } } } } return annotationURI; } /** * @param node * @param classesSeen * @return true if the node has any EAnnotations with the correct source in its subtree. * There is no need to search the whole subtree, since if there is any simplification on * any node, there are annotations on all nodes above it in the tree. * So it is only necessary to search the top EReferences and EAnnotations */ public static boolean hasSimplificationInSubtree(LabelledEClass node, String annotationURI) { // iterate over all EAttributes and EReferences of the class, looking for annotations with the correct source and the correct path for (Iterator<EStructuralFeature> is = node.eClass().getEStructuralFeatures().iterator();is.hasNext();) { EAnnotation note = is.next().getEAnnotation(annotationURI); if ((note != null) && (note.getDetails().get(node.getPath()) != null)) return true; } return false; } private void trace(String s) {if (tracing) System.out.println(s);} static void message(String s) {System.out.println(s);} }