package com.openMap1.mapper.actions;
import java.io.FileInputStream;
import java.io.InputStream;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.StringTokenizer;
import java.util.Vector;
import org.eclipse.emf.common.util.EList;
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.EDataType;
import org.eclipse.emf.ecore.EModelElement;
import org.eclipse.emf.ecore.EPackage;
import org.eclipse.emf.ecore.EReference;
import org.eclipse.emf.ecore.EStructuralFeature;
import org.eclipse.emf.ecore.EcoreFactory;
import org.eclipse.emf.ecore.EcorePackage;
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.util.GenUtil;
import com.openMap1.mapper.util.ModelUtil;
import com.openMap1.mapper.views.ClassModelView;
import com.openMap1.mapper.views.WorkBenchUtil;
public class MergeModelsAction extends Action implements IAction{
private MapperEditor mapperEditor;
private ClassModelView classModelView;
/** the package which is the root of the model in the class model view */
private EPackage ecoreRoot;
boolean tracing = true;
private void trace(String s) {if (tracing) System.out.println(s);}
// key = package_class_feature
private Hashtable<String,String[]> mergeRecord;
private int numberOfModels;
public MergeModelsAction()
{
super("Combine Ecore Models");
}
public void run()
{
classModelView = WorkBenchUtil.getClassModelView(false);
Vector<EPackage> classModels = new Vector<EPackage>();
Vector<String> messageNames = new Vector<String>();
if (classModelView != null) try
{
ecoreRoot = classModelView.ecoreRoot();
StringTokenizer xt = new StringTokenizer( ecoreRoot.eResource().getURI().toPlatformString(true),"/");
String projectName = xt.nextToken();
String[] exts = {"*.csv"};
String resultLocation = "";
// user selects a csv file of Ecore models to merge with the model in the class model view
String mappingSetURIString = classModelView.mappingSetURI().toString();
mapperEditor = WorkBenchUtil.getMapperEditor(mappingSetURIString);
if (mapperEditor != null)
{
String path = FileUtil.getFilePathFromUser(mapperEditor,exts,
"Select csv file of Ecore models to merge, from project " + projectName,false);
if (!path.equals(""))
{
InputStream inputFile = new FileInputStream(path);
Vector<String> lines = FileUtil.getLines(inputFile);
if (lines.size() > 3) for (int ln = 0; ln < lines.size(); ln++)
{
int cols = 2;
String[] cells = FileUtil.parseCSVLine(cols,lines.get(ln));
// check header
if (ln == 0)
{
String[] header = {"Role","Model File"};
for (int col = 0; col < cols; col++)
if (!header[col].equals(cells[col]))
throw new MapperException ("Column header " + cells[col] + " is not " + header[col]);
}
// location for result model
else if (ln == 1)
{
if (!cells[0].equals("result"))
throw new MapperException("first non-header row must start with 'result' and define the result file");
if (!cells[1].endsWith(".ecore"))
throw new MapperException("Result model must be stored in a .ecore file, not in " + cells[1]);
resultLocation = FileUtil.platformPreface() + projectName + "/" + "ClassModel/" + cells[1];
}
// locations and message names of models to be merged
else if (ln < lines.size())
{
if (!cells[1].endsWith(".ecore"))
throw new MapperException("All models to merge must be stored in a .ecore file, not in " + cells[1]);
String modelLocation = FileUtil.platformPreface() + projectName + "/" + "ClassModel/" + cells[1];
EPackage model = FileUtil.getClassModel(FileUtil.absoluteLocation(modelLocation));
classModels.add(model);
messageNames.add(ModelUtil.getMIFAnnotation(model, "messageName"));
}
}
else if (lines.size() < 3)
{
throw new MapperException ("csv file needs to have at least 4 rows - header, result, 2 inputs.");
}
}
}
numberOfModels = classModels.size();
if (numberOfModels > 1)
{
// check that all models have the same top package name (e.g. 'cda')
String topPackageName = commonTopPackage(classModels);
// start the new model that will be made by a merge
EPackage newModel = EcoreFactory.eINSTANCE.createEPackage();
newModel.setName(topPackageName);
newModel.setNsPrefix(ecoreRoot.getNsPrefix());
newModel.setNsURI(ecoreRoot.getNsURI());
/* note the CDAWrapperModel annotation on the top package cannot be used;so the merged class model
* cannot be attached to a mapping set, but must only be used to generate Java API classes.
* Similarly the messageName annotation on the top cda package is arbitrary and will be ignored */
ModelUtil.copyMifAnnotations(ecoreRoot, newModel);
// start the csv file which will record all model merging operations.
int cols = classModels.size() + 5;
String[] header = new String[cols];
header[0] = "Package";
header[1] = "Class";
header[2] = "Feature";
header[3] = "Sources";
header[4] = "Multiplicity";
for (int c = 5; c < cols; c++) header[c] = messageNames.get(c-5);
// keys = package,class,feature
mergeRecord = new Hashtable<String,String[]>() ;
// merge all the models - packages, classes and attributes
for (int m = 0; m < classModels.size(); m++)
mergeModels(newModel, classModels.get(m), m);
// merge all the model associations (they can be done now, as all target classes are now in the merged model)
for (int m = 0; m < classModels.size(); m++)
mergeAssociations(newModel, classModels.get(m), m);
// make any feature optional if it comes from fewer models than its class comes from
makeFeaturesOptional(newModel);
// save the merged model
String savePath = FileUtil.absoluteLocation(resultLocation);
String fullPath = "file:/" + savePath;
ModelUtil.savePackage(fullPath, newModel);
// save the record of the merging process
saveMergeRecords(savePath, header);
WorkBenchUtil.showMessage("Completed","Refresh folder to see merged model in Eclipse");
}
}
catch (Exception ex)
{
WorkBenchUtil.showMessage("Error",ex.getMessage());
ex.printStackTrace();
}
}
/**
* merge two ecore models, leaving the result in the new model
* @param newTopPackage the new model (top package)
* @param sourceTopPackage the source model being merged
* @throws MapperException
*/
private void mergeModels(EPackage newTopPackage,EPackage sourceTopPackage, int modelNumber) throws MapperException
{
// loop over packages in the model being merged in
for (Iterator<EPackage> it = sourceTopPackage.getESubpackages().iterator();it.hasNext();)
{
EPackage sourcePack = it.next();
String packName = sourcePack.getName();
EPackage newPack = getSubPackage(newTopPackage,packName);
// if no such package exists in the new model, make one
if (newPack == null)
{
newPack = EcoreFactory.eINSTANCE.createEPackage();
newPack.setName(packName);
newPack.setNsPrefix(sourcePack.getNsPrefix());
newPack.setNsURI(sourcePack.getNsURI());
copyAnnotations(sourcePack, newPack);
newTopPackage.getESubpackages().add(newPack);
trace("Making package " + packName);
}
// record that this package came from this model
noteMerge(packName,"_","_","","Y",modelNumber);
// merge classes and their attributes from this package
mergeClasses(newPack, sourcePack, modelNumber);
}
}
private boolean containsEntryClass(EPackage thePackage)
{
boolean contains = false;
for (Iterator<EClassifier> it = thePackage.getEClassifiers().iterator();it.hasNext();)
if (ModelUtil.getMIFAnnotation(it.next(), "entry") != null) contains = true;
return contains;
}
/**
*
* @param newPack
* @param sourcePack
* @param modelNumber
* @throws MapperException
*/
private void mergeClasses(EPackage newPack, EPackage sourcePack, int modelNumber) throws MapperException
{
for (Iterator<EClassifier> it = sourcePack.getEClassifiers().iterator(); it.hasNext();)
{
EClassifier next = it.next();
if (next instanceof EClass)
{
EClass sourceClass = (EClass)next;
String className = sourceClass.getName();
EClass newClass = (EClass)newPack.getEClassifier(className);
// if the class is not in the merged model, make it with all the annotations of the original
if (newClass == null)
{
newClass = EcoreFactory.eINSTANCE.createEClass();
newClass.setName(className);
copyAnnotations(sourceClass,newClass);
newPack.getEClassifiers().add(newClass);
trace("Making class " + className + " in package " + newPack.getName());
}
// otherwise, append to the documentation of the class in the new model
else appendDocumentation(newClass,sourceClass);
// record that this class came from this model
noteMerge(newPack.getName(),className,"_","","Y",modelNumber);
// merge attributes of the class
mergeAttributes(newClass,sourceClass,modelNumber);
}
}
}
/**
* copy or check all attributes from a source class to a target class
* @param sourceClass
* @param targetClass
* @throws MapperException
*/
private void mergeAttributes(EClass newClass,EClass sourceClass, int modelNumber) throws MapperException
{
String className = sourceClass.getName();
// find all attributes of the source class
for (Iterator<EStructuralFeature> it = sourceClass.getEStructuralFeatures().iterator(); it.hasNext();)
{
EStructuralFeature next = it.next();
if (next instanceof EAttribute)
{
EAttribute sourceAtt = (EAttribute)next;
String attName = sourceAtt.getName();
EStructuralFeature newAtt = newClass.getEStructuralFeature(attName);
/* if the target class has a feature of the same name, check it is an attribute with type;
* and broaden the multiplicity if necessary */
if (newAtt != null)
{
if (newAtt instanceof EReference)
throw new MapperException("EReference of master class '" + className + "' has the same name as attribute '" + attName + "'");
if (!newAtt.getEType().getName().equals(sourceAtt.getEType().getName()))
throw new MapperException("Attribute '" + attName + "' of master class '" + className + "' has wrong type ");
if (sourceAtt.getLowerBound() == 0) newAtt.setLowerBound(0); // broaden the allowed multiplicity
// add to the documentation of the new model attribute
appendDocumentation(newAtt,sourceAtt);
}
// if the target class does not have the attribute, add it.
else if (newAtt == null)
{
newAtt = EcoreFactory.eINSTANCE.createEAttribute();
newAtt.setName(attName);
newAtt.setLowerBound(sourceAtt.getLowerBound());
newAtt.setEType(sourceAtt.getEType());
copyAnnotations(sourceAtt, newAtt);
newClass.getEStructuralFeatures().add(newAtt);
}
// record that this attribute came from this model
noteMerge(newClass.getEPackage().getName(),newClass.getName(),attName,multString(newAtt),multString(sourceAtt),modelNumber);
}
}
}
/**
*
* @param newTopPackage
* @param sourceTopPackage
* @param modelNumber
* @throws MapperException
*/
private void mergeAssociations(EPackage newTopPackage,EPackage sourceTopPackage, int modelNumber) throws MapperException
{
// loop over all packages of the source model
for (Iterator<EPackage> it = sourceTopPackage.getESubpackages().iterator();it.hasNext();)
{
EPackage sourcePackage = it.next();
EPackage newPackage = getSubPackage(newTopPackage,sourcePackage.getName());
// loop over all classes in this package
for (Iterator<EClassifier> iu = sourcePackage.getEClassifiers().iterator();iu.hasNext();)
{
EClassifier next = iu.next();
if (next instanceof EClass)
{
EClass sourceClass = (EClass)next;
EClass newClass = (EClass)newPackage.getEClassifier(sourceClass.getName());
// loop over all associations of this class
for (Iterator<EStructuralFeature> iv = sourceClass.getEStructuralFeatures().iterator();iv.hasNext();)
{
EStructuralFeature feat = iv.next();
if (feat instanceof EReference)
{
EReference sourceRef = (EReference)feat;
EClass sourceTarget = (EClass)sourceRef.getEType();
String targetClassName = sourceTarget.getName();
String targetPackageName = sourceTarget.getEPackage().getName();
trace("Association to " + targetClassName + " in package " + targetPackageName);
EStructuralFeature newFeat = newClass.getEStructuralFeature(sourceRef.getName());
EReference newRef = null;
// make a new association if necessary
if (newFeat == null)
{
newRef = EcoreFactory.eINSTANCE.createEReference();
newRef.setName(sourceRef.getName());
newRef.setLowerBound(sourceRef.getLowerBound());
newRef.setUpperBound(sourceRef.getUpperBound());
newRef.setContainment(sourceRef.isContainment());
copyAnnotations(sourceRef,newRef);
EPackage newPack = getSubPackage(newTopPackage,targetPackageName);
EClass newTarget = (EClass)newPack.getEClassifier(targetClassName);
newRef.setEType(newTarget);
newClass.getEStructuralFeatures().add(newRef);
}
else if (newFeat instanceof EReference)
{
newRef = (EReference)newFeat;
// checks to make when there is an existing association
EClass newTarget = (EClass)newRef.getEType();
if (!newTarget.getName().equals(sourceTarget.getName()))
throw new MapperException("Association " + newRef.getName()
+ " of class " + newClass.getName() + " has target class names "
+ newTarget.getName() + " and " + sourceTarget.getName());
if (!newTarget.getEPackage().getName().equals(sourceTarget.getEPackage().getName()))
throw new MapperException("Association " + newRef.getName()
+ " of class " + newClass.getName() + " has target package names "
+ newTarget.getEPackage().getName() + " and " + sourceTarget.getEPackage().getName());
// broaden multiplicities
if (sourceRef.getLowerBound() == 0) newRef.setLowerBound(0);
if (sourceRef.getUpperBound() == -1) newRef.setUpperBound(-1);
// append to the documentation of the merged model association
appendDocumentation(newRef,sourceRef);
}
else if (!(newFeat instanceof EReference))
{
WorkBenchUtil.showMessage("Association Error", "Feature " + sourceRef.getName()
+ " of class " + targetClassName + " is also an attribute.");
}
// record that this attribute came from this model
if (newRef != null)
noteMerge(newClass.getEPackage().getName(),newClass.getName(),newRef.getName(),multString(newRef),multString(sourceRef),modelNumber);
}
}
}
}
}
}
/**
* make any feature optional if it comes from fewer models than its class
* @param newModel
*/
private void makeFeaturesOptional(EPackage newModel) throws MapperException
{
for (Iterator<EPackage> it = newModel.getESubpackages().iterator();it.hasNext();)
{
EPackage thePack = it.next();
for (Iterator<EClassifier> iu = thePack.getEClassifiers().iterator();iu.hasNext();)
{
EClass theClass = (EClass)iu.next();
String classKey = thePack.getName() + ";" + theClass.getName();
String[] classRec = mergeRecord.get(classKey + ";_"); // should not be null
if (classRec == null) throw new MapperException("No merge record for class " + classKey);
int classCount = new Integer(classRec[3]).intValue();
for (Iterator<EStructuralFeature> iv = theClass.getEStructuralFeatures().iterator(); iv.hasNext();)
{
EStructuralFeature feat = iv.next();
String featureKey = classKey + ";" + feat.getName();
String[] featRec = mergeRecord.get(featureKey); // should not be null
if (featRec == null) throw new MapperException("No merge record for feature " + featureKey);
int featCount = new Integer(featRec[3]).intValue();
if (featCount < classCount)
{
feat.setLowerBound(0);
noteMerge(thePack.getName(), theClass.getName(), feat.getName(), multString(feat), "", -1);
}
}
}
}
}
/**
*
* @param topPackage
* @param packageName
* @return
*/
private EPackage getSubPackage(EPackage topPackage, String packageName)
{
EPackage newPackage = null;
for (Iterator<EPackage> ix = topPackage.getESubpackages().iterator();ix.hasNext();)
{
EPackage pack = ix.next();
if (pack.getName().equals(packageName)) newPackage = pack;
}
return newPackage;
}
/**
*
* @param from
* @param to
*/
private void copyAnnotations(EModelElement from, EModelElement to)
{
ModelUtil.copySomeAnnotations(from, to, ModelUtil.genModelURI());
ModelUtil.copySomeAnnotations(from, to, ModelUtil.mifNamespaceURI());
}
/**
* keep appending to the documentation of the first model element
* @param firstEl
* @param secondEl
*/
private void appendDocumentation(EModelElement firstEl,EModelElement secondEl)
{
EAnnotation firstAnn = firstEl.getEAnnotation(ModelUtil.genModelURI());
String firstDoc = null;
if (firstAnn != null) firstDoc = firstAnn.getDetails().get("documentation");
EAnnotation secondAnn = secondEl.getEAnnotation(ModelUtil.genModelURI());
String secondDoc = null;
if (secondAnn != null) secondDoc = secondAnn.getDetails().get("documentation");
// append documentation strings if they are both non-null
String fullDoc = null;
if (firstDoc == null) fullDoc = secondDoc;
if (secondDoc == null) fullDoc = firstDoc;
if ((firstDoc != null) && (secondDoc != null)) fullDoc = firstDoc + secondDoc;
if (fullDoc != null) firstEl.getEAnnotation(ModelUtil.genModelURI()).getDetails().put("documentation",fullDoc);
}
/**
*
* @param feat
* @return
*/
private String multString(EStructuralFeature feat)
{
String mult = new Integer(feat.getLowerBound()).toString();
if (feat instanceof EReference) mult = mult + ".." + new Integer(((EReference)feat).getUpperBound()).toString();
else mult = "min" + mult; // to make all values align right in Excel
return mult;
}
/**
* create a new row for the csv record of merge operations if necessary, and update the row
* @param packageName the package in the merged model being created or updated
* @param className the class in the merged model being created or updated
* @param featureName the feature (attribute or association) in the merged model being created or updated
* @param mergedMult the new merged multiplicity or existence flag (eg '0' or '1' for an attribute; '0..*' for an association; 'Y' for a class or package)
* @param modelMult the multiplicity or existence flag from this model
* @param model integer index of the model being merged in
*/
private void noteMerge(String packageName, String className, String featureName, String mergedMult, String modelMult, int model)
{
String key = packageName + ";" + className + ";" + featureName;
String[] row = mergeRecord.get(key);
// make a new row if necessary; every cell must be populated and so non-null
if (row == null)
{
row = new String[numberOfModels + 5];
row[0] = packageName;
row[1] = className;
row[2] = featureName;
row[3] = "0";
// row[4] will be written below
for (int m = 0; m < numberOfModels; m++) row[5 + m] = ""; // empty all model columns
}
// updates to make, whether or not the row existed before
row[4] = mergedMult; // changed by makeFeaturesOptional, as well as other calls
if (model > -1) // only makeFeaturesOptional uses model = -1, because it is not adding a model
{
row[3] = new Integer(new Integer(row[3]).intValue() + 1).toString();
row[5 + model] = modelMult;
}
mergeRecord.put(key, row);
}
/**
* check that all class models have the same top package name, and return it.
* @param classModels
* @return
* @throws MapperException
*/
private String commonTopPackage(Vector<EPackage> classModels) throws MapperException
{
String packageName = classModels.get(0).getName();
for (int i = 1; i < classModels.size(); i++)
{
String name = classModels.get(i).getName();
if (!name.equals(packageName)) throw new MapperException("Class models have differing top package names: " + packageName + " and " + name);
}
return packageName;
}
private String extractFileRoot(String path)
{
String fileName = "";
StringTokenizer st = new StringTokenizer(path,"/\\");
while (st.hasMoreTokens()) fileName = st.nextToken();
StringTokenizer su = new StringTokenizer(fileName,".");
String root = su.nextToken();
return root;
}
/**
* save the csv file recording the merge operations (unordered; need to sort in Excel)
* @param savePath
* @param header
* @throws MapperException
*/
private void saveMergeRecords(String savePath, String[] header) throws MapperException
{
Vector<String[]> rows = new Vector<String[]>();
rows.add(header);
for (Enumeration<String[]> en = mergeRecord.elements();en.hasMoreElements();)
rows.add(en.nextElement());
String csvPath = savePath.substring(0, savePath.length() - ".ecore".length()) + ".csv";
FileUtil.writeCSVFile(csvPath, rows);
}
}