package com.openMap1.mapper.actions;
import java.io.IOException;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.StringTokenizer;
import java.util.Vector;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IFolder;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.emf.common.util.URI;
import org.eclipse.emf.ecore.EAttribute;
import org.eclipse.emf.ecore.EClass;
import org.eclipse.emf.ecore.EClassifier;
import org.eclipse.emf.ecore.EObject;
import org.eclipse.emf.ecore.EPackage;
import org.eclipse.emf.ecore.EReference;
import org.eclipse.emf.ecore.EStructuralFeature;
import org.eclipse.emf.ecore.resource.Resource;
import org.eclipse.jface.action.IAction;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.ui.IObjectActionDelegate;
import org.eclipse.xsd.XSDSchema;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import com.openMap1.mapper.core.MapperException;
import com.openMap1.mapper.structures.MappableAssociation;
import com.openMap1.mapper.structures.StructureDefinition;
import com.openMap1.mapper.structures.XSDStructure;
import com.openMap1.mapper.util.EclipseFileUtil;
import com.openMap1.mapper.util.FileUtil;
import com.openMap1.mapper.util.GenUtil;
import com.openMap1.mapper.util.ModelUtil;
import com.openMap1.mapper.util.XMLUtil;
import com.openMap1.mapper.views.WorkBenchUtil;
import com.openMap1.mapper.AssocEndMapping;
import com.openMap1.mapper.AssocMapping;
import com.openMap1.mapper.AttributeDef;
import com.openMap1.mapper.ConditionTest;
import com.openMap1.mapper.CrossCondition;
import com.openMap1.mapper.ElementDef;
import com.openMap1.mapper.GlobalMappingParameters;
import com.openMap1.mapper.MappedStructure;
import com.openMap1.mapper.MapperFactory;
import com.openMap1.mapper.MultiWay;
import com.openMap1.mapper.Namespace;
import com.openMap1.mapper.NodeMappingSet;
import com.openMap1.mapper.ObjMapping;
import com.openMap1.mapper.PropMapping;
import com.openMap1.mapper.ValueCondition;
/**
*
* @author robert
* Class to make an XML schema for a persisted instance of the selected ecore model.
* It has two subclasses MakeEcoreInstanceSchemaDelegate and MakeAlternativeInstanceSchemaDelegate,
* which respectively make the schema for a conventional Ecore instance, and for
* an alternative form of persisted Ecore instance which can be mapped more easily.
* The two subclasses differ only in the value of isAlternateSchema();
* all the code is here (so far)
*/
public abstract class MakeInstanceSchemaDelegate extends MapperActionDelegate implements IObjectActionDelegate {
/**
* @return false if making a schema for a plain ECore model instance;
* true if making a schema for the alternate 'mapper-friendly' form of instance,
* and then making the mappings onto the Ecore model
*/
protected abstract boolean isAlternateSchema();
/**
* prefix used in schemas for the XML Schema namespace
*/
private static String xmlSchemaPrefix = "xs";
/**
* suffix added to ECore class names to create the corresponding complex type name
*/
private static String typeSuffix = "_type";
/**
* The name used for an id attribute for all instances. Will be modified so
* it does not clash with any feature name in the model
*/
private String idName = "";
/**
* maximum number of object mappings for one class, when it contains itself by nesting
*/
private int maxClassNestingDepth()
{
return maxClassNestingDepth;
}
// initial value for no self-nesting
private int maxClassNestingDepth = 1;
/**
* certain classes and all their subclasses are to be excluded from the mappings.
* ElementsDefs that represent them are not expanded
*/
private String[] unMappedClasses = {"EAnnotation","EOperation","EGenericType",
"EFactory","EParameter","ETypeParameter","EEnum","EEnumLiteral"};
//-----------------------------------------------------------------------------------------------
// main run method
//-----------------------------------------------------------------------------------------------
public void run(IAction action)
{
tracing = true;
trace("Making Ecore Instance Schema " + isAlternateSchema());
try{
// (1) find and open the Ecore model
if (!(selection instanceof IStructuredSelection))
throw new MapperException("Selection is not structured");
IStructuredSelection structured = (IStructuredSelection)selection;
Object object = structured.getFirstElement();
String fileNameRoot = "";
EPackage topPackage = null;
URI ecoreURI = null;
if (object instanceof IFile) {
IFile file = (IFile) object;
fileNameRoot = new StringTokenizer(file.getName(),".").nextToken();
ecoreURI = URI.createPlatformResourceURI(file.getFullPath().toString(), true);
EObject root = FileUtil.getEMFModelRoot(ecoreURI);
if (root == null)throw new MapperException("Cannot find ecore root");
if (!(root instanceof EPackage))
throw new MapperException("Selected file is not an ecore model");
topPackage = (EPackage)root;
trace("Top package name " + topPackage.getName());
}
else throw new MapperException("Selection is not a file");
// (2) Offer the user a choice of top class for the instance to be described by the schema
EClass topClass = userChooseTopClass(topPackage);
if (topClass == null) return;
else trace("Chosen class: " + topClass.getName());
// (3) make an IFile for the schema; if a previous one exits, delete it (silently!)
IProject project = getSelectedProject();
if (project == null)throw new MapperException("Selected Ecore file is not in a project");
IFolder structureFolder = project.getFolder("Structures");
if (!structureFolder.exists()) throw new MapperException("Selected ecore file is not in a project with a Structures folder");
String schemaFileName = fileNameRoot + ".xsd";
if (isAlternateSchema()) schemaFileName = fileNameRoot + "_alt.xsd";
IFile schemaFile = structureFolder.getFile(schemaFileName);
if (schemaFile.exists()) schemaFile.delete(true, null);
// (4) write the schema
writeSchema(topPackage, topClass, schemaFile);
// (5) for the alternate Ecore persistent form, write the mapping sets
if (isAlternateSchema())
{
EClass selfNested = selfNestedClass(topPackage); // any example self-nested class, if there are any
if (selfNested != null)
{
int depth = userChooseNestingDepth(selfNested);
if (depth == -1) return; // user cancels
maxClassNestingDepth = depth + 1; // maxClassNestingDepth = number of mappings of self-nested classes
fileNameRoot = fileNameRoot + "_" + depth; // different names for mapper files of different nesting depth
}
URI schemaURI = URI.createPlatformResourceURI("/" + project.getName() + "/Structures/" + schemaFileName,true);
XSDSchema theSchema = XSDStructure.getXSDRoot(schemaURI);
XSDStructure altStructure = new XSDStructure(theSchema);
makeMappingSets(topPackage,ecoreURI,topClass,altStructure,schemaURI,project,fileNameRoot);
}
}
catch (MapperException ex)
{showMessage("Unable to make instance schema",ex.getMessage());if (tracing) ex.printStackTrace();}
catch (IOException ex)
{showMessage("Unable to make instance schema",ex.getMessage());}
catch (CoreException ex)
{showMessage("Unable to make instance schema",ex.getMessage());}
}
/**
* @param topPackage the top package of an Ecore class model; show the user the names
* of all classes in all packages of this model
* (note if two classes in different packages have the same name, they are not yet
* disambiguated)
* @return the EClass chosen by the user, or null if he cancels
*/
private EClass userChooseTopClass(EPackage topPackage)
{
EClass chosenClass = null;
Vector<EClass> allClasses = ModelUtil.getAllClasses(topPackage);
Vector<String> classNames = new Vector<String>();
for (Iterator<EClass> it = allClasses.iterator();it.hasNext();)
classNames.add(it.next().getName());
int chosen = WorkBenchUtil.chooseOneString("Choose the root class of the Ecore instance", targetPart, classNames);
if (chosen > -1) chosenClass = allClasses.get(chosen);
return chosenClass;
}
/**
* @param selfNested a class which has a containment association to itself
* Allow the user to choose a depth of self-nesting of such classes, to be supported
* in the auto-generated mappings
* @return the depth of nesting (0 for no nesting etc.) or -1 if the user cancels
*/
private int userChooseNestingDepth(EClass selfNested)
{
int maxDepthOffered = 6;
Vector<String> depths = new Vector<String>();
for (int d = 0; d < maxDepthOffered + 1; d++)
depths.add(new Integer(d).toString());
int chosen = WorkBenchUtil.chooseOneString("Depth of nesting in the mappings for self-containing classes such as '"
+ selfNested.getName() + "'", targetPart, depths);
return chosen;
}
/**
* if the model has any EClasses linked to themselves by containment
* associations, return any one of them
* @param topPackage
* @return an EClass which has a direct containment association to itself, if there are any
*/
private EClass selfNestedClass(EPackage topPackage)
{
EClass selfNested = null;
Vector<EClass> allClasses = ModelUtil.getAllClasses(topPackage);
for (Iterator<EClass> it = allClasses.iterator();it.hasNext();)
{
EClass next = it.next();
for (Iterator<EReference> ie = next.getEAllReferences().iterator();ie.hasNext();)
{
EReference ref = ie.next();
EClassifier ec = ref.getEType();
if ((ref.isContainment()) && (ec instanceof EClass))
{
EClass superC = (EClass)ec;
if (superC.isSuperTypeOf(next)) selfNested = next;
}
}
}
return selfNested;
}
//-----------------------------------------------------------------------------------------------
// making schemas for Ecore instances - conventional and alternate
//-----------------------------------------------------------------------------------------------
/**
* @param topPackage the top package of the Ecore model whose instance schema is being written
* @param topClass the root class of instances whose schema is being written
* @param schemaFile the IFile that the schema is to be written to
* create the schema and write it to the IFile
*/
private void writeSchema(EPackage topPackage, EClass topClass, IFile schemaFile) throws MapperException
{
Document doc = XMLUtil.makeOutDoc();
Element schemaRoot = schemaElement(doc,"schema");
schemaRoot.setAttribute("targetNamespace", topPackage.getNsURI());
schemaRoot.setAttribute("xmlns:" + topPackage.getNsPrefix(), topPackage.getNsURI());
schemaRoot.setAttribute("xmlns:xmi", XMLUtil.XMIURI);
schemaRoot.setAttribute("elementFormDefault", "unqualified");
doc.appendChild(schemaRoot);
// Import the XMI schema
Element xmiImport = schemaElement(doc,"import");
xmiImport.setAttribute("namespace", XMLUtil.XMIURI);
xmiImport.setAttribute("schemaLocation", XMLUtil.XMISchemaLocation);
schemaRoot.appendChild(xmiImport);
// make an element declaration for the instance of the root class
Element topClassElementDefinition = schemaElement(doc,"element");
topClassElementDefinition.setAttribute("name", topClass.getName());
topClassElementDefinition.setAttribute("type", topClass.getName() + typeSuffix);
schemaRoot.appendChild(topClassElementDefinition);
// pick a name for the id attribute, which will not clash with any feature name in the model
idName = nonClashIdName(topPackage);
// make a type definition for the top class - and recursively, all those it contains
Hashtable<String,String> classesWritten = new Hashtable<String,String>();
appendTypeDefinition(doc,schemaRoot,topClass,topClass,classesWritten, true);
// write out to the IFile
EclipseFileUtil.writeOutputResource(doc, schemaFile, true);
}
/**
* @param doc the schema Document
* @param schemaRoot the schema element
* @param aClass a class in the class model (reached from the root class by containment relations)
* @param classesWritten table of classes for which types have been defined - to avoid repetition
* @param isTopClass true if this is the class of the root element of the instance
* Make a complex type for the class and attach it to the schema; recursively do all contained classes
* @throws MapperException
*/
private void appendTypeDefinition(Document doc,Element schemaRoot,EClass topClass,EClass aClass,
Hashtable<String,String> classesWritten, boolean isTopClass)
throws MapperException
{
// make the complex type element and append it to the schema
Element typeElement = schemaElement(doc,"complexType");
typeElement.setAttribute("name", aClass.getName() + typeSuffix);
Element extendElement = typeElement;
schemaRoot.appendChild(typeElement);
String className = aClass.getName();
classesWritten.put(className,"1");
/* decide if this complex type extends any other. If there are any superclasses, choose the first
* direct superclass that appears in the instance tree.
* The choice is only made here, so does not have to be reproducible.
* The type for the top class should not extend any other type, even if the top class has superclasses. */
EClass mainSuperClass = null;
// if (!isTopClass)
for (Iterator<EClass> ic = aClass.getESuperTypes().iterator();ic.hasNext();)
{
EClass superClass = ic.next();
// pick the first direct superclass which is at the inner end of a containment in the instance
if ((typeAppearsInSchema(topClass, superClass)) && (mainSuperClass == null))
{
mainSuperClass = superClass;
Element content = schemaElement(doc,"complexContent");
typeElement.appendChild(content);
Element extension = schemaElement(doc,"extension");
extension.setAttribute("base", mainSuperClass.getName() + typeSuffix);
content.appendChild(extension);
extendElement = extension;
}
}
/* make a sequence of nested Elements for all containment associations,
* and recursively define types for all the target classes. */
Element sequence = schemaElement(doc,"sequence");
boolean sequenceNonEmpty = false;
for (Iterator<EReference> ir = aClass.getEAllReferences().iterator();ir.hasNext();)
{
EReference ref = ir.next();
String refName = ref.getName();
// don't include anything in the schemas for derived features
if ((ref.isContainment()) && (ref.getEType() instanceof EClass) && (!ref.isDerived()))
{
EClass targetSuperClass = (EClass)ref.getEType();
if (!inheritedFrom(ref,mainSuperClass)) // don't add those associations already inherited
{
sequenceNonEmpty = true;
Element childElement = schemaElement(doc,"element");
childElement.setAttribute("name", refName);
childElement.setAttribute("type", targetSuperClass.getName() + typeSuffix);
if (ref.getUpperBound() == -1) childElement.setAttribute("maxOccurs", "unbounded");
childElement.setAttribute("minOccurs", new Integer(ref.getLowerBound()).toString());
sequence.appendChild(childElement);
}
// make type definitions for all subclasses of the target superclass, not repeating any type
for (Iterator<EClass> ic = ModelUtil.getAllSubClasses(targetSuperClass).iterator();ic.hasNext();)
{
EClass targetClass = ic.next();
if (classesWritten.get(targetClass.getName()) == null)
appendTypeDefinition(doc,schemaRoot,topClass,targetClass, classesWritten, false); // false = not the top class
}
}
}
if (sequenceNonEmpty) extendElement.appendChild(sequence);
/* make attribute declarations for all non-containment associations
* which are not the inverses of containment relations. */
for (Iterator<EReference> ir = aClass.getEAllReferences().iterator();ir.hasNext();)
{
EReference ref = ir.next();
if ((!inheritedFrom(ref,mainSuperClass)) // don't add those associations already inherited
&& (!ref.isContainment()) // containment relations have already been done
&& (ref.getEType() instanceof EClass) // FIXME - what should we do with these?
&& (!ref.isDerived()) // no derived features - they are not persisted
&& ((ref.getEOpposite() == null)||(!ref.getEOpposite().isContainment())))
{
Element nonConRef = schemaElement(doc,"attribute");
nonConRef.setAttribute("name",ref.getName());
if (ref.getLowerBound() == 1) nonConRef.setAttribute("use", "required");
nonConRef.setAttribute("type", "xs:string");
extendElement.appendChild(nonConRef);
}
}
// make attribute declarations for all properties of the class
for (Iterator<EAttribute> ia = aClass.getEAllAttributes().iterator();ia.hasNext();)
{
EAttribute att = ia.next();
// don't add those attributes already inherited or derived
if ((!inheritedFrom(att,mainSuperClass)) && (!att.isDerived()))
{
Element attEl = schemaElement(doc,"attribute");
attEl.setAttribute("name",att.getName());
if (att.getLowerBound() == 1) attEl.setAttribute("use", "required");
attEl.setAttribute("type", "xs:string");
extendElement.appendChild(attEl);
}
}
/* for the alternate Ecore serialisation, declare a non-clashing 'id' attribute
* for each instance of the class. Only do this for classes which are not subclasses;
* subclasses inherit it from them. */
if ((isAlternateSchema()) && (mainSuperClass == null))
{
Element attEl = schemaElement(doc,"attribute");
attEl.setAttribute("name",idName);
attEl.setAttribute("type", "xs:string");
extendElement.appendChild(attEl);
}
}
/**
*
* @param rootClass the root class for an instance of the model
* @param aClass a class
* @return true if instances of the class may appear in the tree below the root class
*/
private boolean appearsInInstance(EClass rootClass, EClass aClass)
{
return ((!aClass.isAbstract()) && (typeAppearsInSchema(rootClass, aClass)));
}
/**
* @param rootClass the class of the root of the instance tree
* @param aClass any EClass
* @return true if there are containment association to the class in the tree underneath
* the root class (so that anty non-abstract subclasses of the class may be represented
* in the instance)
*/
private boolean typeAppearsInSchema(EClass rootClass, EClass aClass)
{
Hashtable<String,String> classesTried = new Hashtable<String,String>();
// initially true if the class appears anywhere in the containment tree under the top class
boolean appears = typeAppearsInSchema(rootClass, aClass, classesTried);
// but make it false if the class or any of its superclasses is on a list of classes not to map
if (GenUtil.inArray(aClass.getName(), unMappedClasses)) appears = false;
for (Iterator<EClass> ic = aClass.getEAllSuperTypes().iterator(); ic.hasNext();)
if (GenUtil.inArray(ic.next().getName(), unMappedClasses)) appears = false;
return appears;
}
/**
* recursive descent of the containment association tree, not revisiting any class
* @param aClass current class in the tree being checked for equality with the test class
* @param bClass class being tested for inclusion in the tree
* @param classesTried classes in the tree already tested
* @return true if bClass is in the tree below aClass
*/
private boolean typeAppearsInSchema(EClass aClass, EClass bClass, Hashtable<String,String> classesTried)
{
if (aClass.getName().equals(bClass.getName())) return true;
classesTried.put(aClass.getName(), "1");
for (Iterator<EReference> ir = aClass.getEAllReferences().iterator();ir.hasNext();)
{
EReference ref = ir.next();
if ((ref.isContainment()) && (ref.getEType() instanceof EClass))
{
EClass targetSuperClass = (EClass)ref.getEType();
// try all subclasses of the target superclass
for (Iterator<EClass> ic = ModelUtil.getAllSubClasses(targetSuperClass).iterator();ic.hasNext();)
{
EClass targetClass = ic.next();
if ((classesTried.get(targetClass.getName()) == null) &&
(typeAppearsInSchema(targetClass,bClass,classesTried))) return true;
}
}
}
return false;
}
/**
*
* @param feature an EReference or EAttribute
* @param superClass one of the superclasses of the class; or null
* @return true if the EReference or EAttribute is inherited from the superclass
*/
private boolean inheritedFrom(EStructuralFeature feature, EClass superClass)
{
if (superClass == null) return false;
return (superClass.getEStructuralFeature(feature.getName()) != null);
}
/**
*
* @param doc the schema document
* @param localName local name of an Element
* @return the element in the XML Schema namespace
* @throws MapperException
*/
private Element schemaElement(Document doc, String localName) throws MapperException
{return XMLUtil.NSElement(doc, xmlSchemaPrefix, localName, XMLUtil.SCHEMAURI);}
/**
* @param topPackage the top package of an ecore model
* @return the name for an object unique identifier 'id' attribute, which does not clash
* with the name of any other structural feature of any class in the model
*/
public static String nonClashIdName(EPackage topPackage)
{
String idRoot = "alt_id";
String idName = idRoot;
Vector<String> featureNames = new Vector<String>();
getFeatureNames(featureNames,topPackage);
int index = 1;
while (GenUtil.inVector(idName, featureNames))
{
idName = idRoot + "_" + index;
index++;
}
return idName;
}
/**
* @param featureNames: to be built up into a list of all features of all classes,
* in this package and all its sub-packages. Duplicate feature names are allowed.
* @param thePackage
*/
private static void getFeatureNames(Vector<String> featureNames,EPackage thePackage)
{
for (Iterator<EClassifier> ic = thePackage.getEClassifiers().iterator();ic.hasNext();)
{
EClassifier ec = ic.next();
if (ec instanceof EClass)
{
EClass aClass = (EClass)ec;
for (Iterator<EStructuralFeature> it = aClass.getEAllStructuralFeatures().iterator();it.hasNext();)
featureNames.add(it.next().getName());
}
}
for (Iterator<EPackage> ip = thePackage.getESubpackages().iterator();ip.hasNext();)
getFeatureNames(featureNames,ip.next());
}
//-----------------------------------------------------------------------------------------------
// making mapping sets for the alternate Ecore Serialisation
//-----------------------------------------------------------------------------------------------
/**
* key = qualified class name
* value = Vector of XPaths to object mappings, with subsets in sequence "", "s1", "s2",.....
*/
private Hashtable<String,Vector<String>> mappingPaths;
/**
* usually called _after_ the path for the mapping has been allocated;
* but if called before any mappings are made, returns the first subset "".
* @param theClass an EClass
* @return the subset for the mapping
*/
private String getLatestSubset(EClass theClass)
{
String subset = "";
String qualifiedClassName = ModelUtil.getQualifiedClassName(theClass);
Vector<String> paths = mappingPaths.get(qualifiedClassName);
if (paths == null) return subset;
int index = paths.size() -1;
if (index > 0) subset = "s" + index; // after "", the sequence starts at "s1"
return subset;
}
/**
* @param theClass a class which may (or may not) be the parent of the current class
* @param path the path to a node where the current class is represented
* @return the subset of the parent class as mapped to the parent node, if it is so mapped;
* or null if it is not
*/
private String getParentNodeSubset(EClass theClass,String path)
{
String innerStep = "";
StringTokenizer st = new StringTokenizer(path,"/");
while (st.hasMoreTokens()) innerStep = st.nextToken();
String parentPath = path.substring(0,path.length() - innerStep.length() - 1);
String qualifiedClassName = ModelUtil.getQualifiedClassName(theClass);
Vector<String> paths = mappingPaths.get(qualifiedClassName);
if (paths == null) return null; // no mappings yet to the parent class - obscure case
boolean found = false;
String subset = ""; // return when i= 0 test below succeeds
for (int i = 0; i < paths.size();i++)
if (paths.get(i).equals(parentPath))
{
found = true;
if (i > 0) subset = "s" + i; // after "", the sequence starts at "s1"
}
if (!found) return null; // 'parent' class has no mapping on the parent node - can occur
return subset;
}
/**
*
* @param topPackage
* @param ecoreURI
* @param topClass
* @param altStructure
* @param schemaURI
* @param projectName
*/
private void makeMappingSets(EPackage topPackage,URI ecoreURI,EClass topClass,
StructureDefinition altStructure,URI schemaURI, IProject project, String fileNameRoot)
throws MapperException
{
String fileName = fileNameRoot + "_alt.mapper";
IFolder mappingsFolder = project.getFolder("MappingSets");
if (!mappingsFolder.exists()) throw new MapperException("No MappingSets folder in project");
if ((mappingsFolder.findMember(fileName)!= null ) && (mappingsFolder.findMember(fileName).exists()))
{
boolean confirm = WorkBenchUtil.askConfirm("Mapping set '" + fileName + "' already exists",
"Do you want to replace the existing mapping set?");
if (!confirm) return;
}
String mapperPath = "/" + project.getName() + "/MappingSets/" + fileName;
URI uri = URI.createURI(mapperPath);
Resource mappingSet = ModelUtil.makeNewMappingSet(uri);
MappedStructure mappedStructure = (MappedStructure)mappingSet.getContents().get(0);
mappedStructure.setUMLModelURL(ecoreURI.toString());
mappedStructure.setStructureURL(schemaURI.toString());
mappedStructure.setStructureDefinition(altStructure);
completePackageNamespaces(topPackage,mappedStructure.getMappingParameters());
String topElementName = topPackage.getNsPrefix() + ":" + topClass.getName();
String topElementType = topClass.getName() + typeSuffix;
mappedStructure.setTopElementName(topElementName);
mappedStructure.setTopElementType(topElementType);
mappedStructure.setRootElement(altStructure.typeStructure(topElementType));
ElementDef rootElement = mappedStructure.getRootElement();
rootElement.setName(topElementName);
rootElement.setExpanded(true);
// recursive descent of the tree, making mappings
mappingPaths = new Hashtable<String,Vector<String>>();
String path = "/" + topElementName;
mapClasses(mappedStructure, rootElement, topPackage, topClass, topClass, altStructure, path);
// for classes with more than one mapped subset, ensure non-containment associations have all mappings
completeAssociationMappings(rootElement);
// save the mapping set in the MappingSets folder
ModelUtil.saveMappingSet(mappingSet);
}
/**
* @param topPackage the top package of a class model
* @param gmp GlobalMappingParameters of a mapping set
* Ensure that the namespace set of the mapping parameters contains the namespace
* of every package in the model
*/
private void completePackageNamespaces(EPackage topPackage, GlobalMappingParameters gmp)
throws MapperException
{
Hashtable<String,String> namespaces = new Hashtable<String,String>();
// note the namespaces already in the mapping set
for (Iterator<Namespace> in = gmp.getNameSpaces().iterator();in.hasNext();)
{
Namespace ns = in.next();
namespaces.put(ns.getURL(), ns.getPrefix());
}
// recursive descent of all packages, adding their namespaces
addPackageNamespace(topPackage,namespaces,gmp);
}
/**
*
* @param aPackage
* @param namespaces
* @param gmp
* @throws MapperException
* recursive descent of all packages, adding their namespaces to the set.
*/
private void addPackageNamespace(EPackage aPackage, Hashtable<String,String> namespaces, GlobalMappingParameters gmp)
throws MapperException
{
String uri = aPackage.getNsURI();
String prefix = aPackage.getNsPrefix();
if (uri == null) throw new MapperException("Null namespace URI in package '" + aPackage.getName() + "'");
if (prefix == null) throw new MapperException("Null namespace prefix in package '" + aPackage.getName() + "'");
String existingPrefix = namespaces.get(uri);
if (existingPrefix == null)
{
Namespace ns = MapperFactory.eINSTANCE.createNamespace();
ns.setPrefix(prefix);
ns.setURL(uri);
gmp.getNameSpaces().add(ns);
}
else if (existingPrefix != null)
{
if (!existingPrefix.equals(prefix))
throw new MapperException("Previous prefix '" + existingPrefix + "' clashes with prefix '"
+ prefix + "' of package '" + aPackage.getName() + "'");
}
for (Iterator<EPackage> ip = aPackage.getESubpackages().iterator(); ip.hasNext();)
addPackageNamespace(ip.next(),namespaces,gmp);
}
/**
* @param paths Vector of string paths
* @param path a path
* @return the number of paths in the Vector that are sub-paths of the other path
*/
private int subPaths(Vector<String> paths, String path)
{
int count = 0;
for (Iterator<String> it = paths.iterator();it.hasNext();)
if (path.startsWith(it.next())) count++;
return count;
}
/**
*
* @param mappedStructure
* @param mappedElement
* @param topPackage
* @param mappedClass
* @param topClass
* @param altStructure
* @param path
* @throws MapperException
*/
private void mapClasses(MappedStructure mappedStructure, ElementDef mappedElement,
EPackage topPackage, EClass mappedClass, EClass topClass,
StructureDefinition altStructure, String path)
throws MapperException
{
/* Count the sub-paths of the current path mapped to this class,
* to limit self-nesting depth */
Vector<String> paths = mappingPaths.get(ModelUtil.getQualifiedClassName(mappedClass));
if ((paths != null) && (subPaths(paths, path) > maxClassNestingDepth() - 1)) return;
trace("Map class " + mappedClass.getName() + " at path " + path);
// add a NodeMappingSet to hold all the object mappings and containment association mappings
NodeMappingSet nodeMappingSet = MapperFactory.eINSTANCE.createNodeMappingSet();
mappedElement.setNodeMappingSet(nodeMappingSet);
/* Add an object mapping to the class and its concrete subclasses;
* if it has more than one concrete subclass, make object mappings conditional on the value of xsi:type */
Vector<EClass> subclasses = ModelUtil.getAllConcreteSubClasses(mappedClass);
for (Iterator<EClass> it = subclasses.iterator(); it.hasNext();)
{
EClass subClass = it.next();
String sqName = ModelUtil.getQualifiedClassName(subClass);
Vector<String> sPaths = mappingPaths.get(sqName);
if (sPaths == null) sPaths = new Vector<String>();
sPaths.add(path);
mappingPaths.put(sqName,sPaths);
String subClassSubset = getLatestSubset(subClass);
String subClassName = subClass.getName();
String subClassPackageName = subClass.getEPackage().getName();
String subClassPackagePrefix = subClass.getEPackage().getNsPrefix();
if (subClassPackageName == null) subClassPackageName= "";
ObjMapping om = MapperFactory.eINSTANCE.createObjMapping();
om.setMappedClass(subClassName);
om.setMappedPackage(subClassPackageName);
om.setSubset(subClassSubset);
// add mapping conditions if necessary
if (subclasses.size() > 1)
{
ValueCondition vc = MapperFactory.eINSTANCE.createValueCondition();
vc.setLeftPath("@xsi:type");
vc.setRightValue(subClassName);
if (subClassPackageName.length() > 0) vc.setRightValue(subClassPackagePrefix + ":" + subClassName);
om.getMappingConditions().add(vc);
}
nodeMappingSet.getObjectMappings().add(om);
// add property mappings for all properties of each subclass
for (Iterator<EAttribute> ia = subClass.getEAllAttributes().iterator();ia.hasNext();)
{
EAttribute att = ia.next();
if (!att.isDerived())
{
String attName = att.getName();
AttributeDef attDef = mappedElement.getNamedAttribute(attName);
if (attDef == null) throw new MapperException("Cannot find AttributeDef for property '"
+ attName + "' of class '" + subClass.getName() + "' at path " + path);
// add the node mapping set only once to each AttributeDef
NodeMappingSet nms = attDef.getNodeMappingSet();
if (nms == null)
{
nms = MapperFactory.eINSTANCE.createNodeMappingSet();
attDef.setNodeMappingSet(nms);
}
PropMapping pm = MapperFactory.eINSTANCE.createPropMapping();
pm.setMappedClass(subClassName);
pm.setMappedPackage(subClassPackageName);
pm.setMappedProperty(attName);
pm.setSubset(subClassSubset);
nms.getPropertyMappings().add(pm);
} // end of if !derived section
} // end of iteration over attributes
/* Iterate over all associations of each subclass */
for (Iterator<EReference> ir = subClass.getEAllReferences().iterator();ir.hasNext();)
{
EReference ref = ir.next();
String refName = ref.getName();
EClassifier target = ref.getEType();
if (!ref.isDerived())
{
if (target instanceof EClass)
{
EClass targetClass = (EClass)target;
// containment relations; take the recursive step to the child elements
trace("EReference " + subClassName + "." + refName + "." + targetClass.getName());
if (ref.isContainment())
{
ElementDef child = mappedElement.getNamedChildElement(refName);
if (child == null) System.out.println("Cannot find child element '"
+ refName + "' of element '" + mappedElement.getName() + "' at path " + path);
// a child element which has already been encountered in another subclass will have been expanded
else if ((!child.isExpanded()) && (typeAppearsInSchema(topClass,targetClass)))
{
expandNode(child,altStructure);
String newPath = path + "/" + child.getName();
// recursive step; depth check for classes that contain themselves is inside the call
mapClasses(mappedStructure, child, topPackage, targetClass, topClass, altStructure, newPath);
}
} // end of isContainment section
// non-containment relations; need to treat all target subclasses separately
else if (!ref.isContainment())
{
for (Iterator<EClass> is = ModelUtil.getAllSubClasses(targetClass).iterator();is.hasNext();)
{
EClass targetSubClass = is.next();
// only include associations if the target class is in the instance
if (appearsInInstance(topClass, targetSubClass))
{
/* common association mapping code, for relations that are non- containments at both ends and
* for relations whose EOpposite is a containment */
AssocMapping am = MapperFactory.eINSTANCE.createAssocMapping();
// the root node does not have a mapped containment association of the top class, or any other associations
StringTokenizer steps = new StringTokenizer(path,"/");
boolean addAssociationMapping = (steps.countTokens() > 1); // reset false in some cases
// end from the other end class to the current class
AssocEndMapping end1 = MapperFactory.eINSTANCE.createAssocEndMapping();
end1.setMappedClass(subClassName);
end1.setMappedPackage(subClassPackageName);
end1.setSubset(subClassSubset);
String roleName = MappableAssociation.NON_NAVIGABLE_ROLE_NAME;
if (ref.getEOpposite() != null) roleName = ref.getEOpposite().getName();
end1.setMappedRole(roleName);
// end from the current class to the other end class
AssocEndMapping end2 = MapperFactory.eINSTANCE.createAssocEndMapping();
end2.setMappedClass(targetSubClass.getName());
String targetPackage = targetSubClass.getEPackage().getName();
if (targetPackage == null) targetPackage = "";
end2.setMappedPackage(targetPackage);
end2.setSubset(getLatestSubset(targetSubClass)); // wrong for self-containment; corrected below
String inverseRole = refName;
if (refName == null) inverseRole = MappableAssociation.NON_NAVIGABLE_ROLE_NAME;
end2.setMappedRole(inverseRole);
trace("made ends");
/* every subclass should have at least one EReference whose opposite is a containment; one
* of these should be a containment in the next outer class.
* this has a simple association mapping on the Element*/
if ( (steps.countTokens() > 1) && // not on outer node
(ref.getEOpposite() != null) && // EOpposite exists
(ref.getEOpposite().isContainment())) // EOpposite is containment
{
end1.setRequiredForObject(true);
/* set the paths between parent and child nodes explicitly, so for that for
* self-nesting associations the default shortest path '.' is not allowed */
end2.setObjectToAssociationPath(mappedElement.getName());
end2.setAssociationToObjectPath("parent::" + parentNodeName(path));
// correct a subset made before, which might be wrong for a self-containment association
String correctSubset = getParentNodeSubset(targetSubClass,path);
if (correctSubset != null)
{
end2.setSubset(correctSubset);
trace("made containment");
}
// a containment in a higher ancestor, not the parent(may occur for nested subsets); add no association mapping
if (correctSubset == null)
{
addAssociationMapping = false;
trace("found no parent mapping to " + targetSubClass.getName());
}
}
/* for associations that are not containments in either direction, initially make one association
* mapping for each subclass, with a cross-condition. Later make one for each subset of each subclass. */
else if (steps.countTokens() > 1)
{
if (ref.getEOpposite() != null) am.setMultiWay(MultiWay.REDUNDANT); // these associations are represented twice
AttributeDef attDef = mappedElement.getNamedAttribute(refName);
if (attDef == null) throw new MapperException("Cannot find AttributeDef for association '"
+ refName + "' at path " + path);
/* long cross-paths to pick up all possible nodes
* are set up in a second pass, when all the object mappings exist;
* the default cross-path might be shorter */
// add the cross-condition on end 2
CrossCondition cc = MapperFactory.eINSTANCE.createCrossCondition();
cc.setLeftPath("@" + refName); // XPath to the reference attribute
cc.setRightPath("@" + idName); // object identifier attribute
if (ref.getUpperBound() == -1) cc.setTest(ConditionTest.CONTAINSASWORD); // the left value contains the right value
end2.getMappingConditions().add(cc);
trace("made long-range");
} // end of 'EOpposite is is non-containment' section
addEnds(am,end1,end2);
if (addAssociationMapping) nodeMappingSet.getAssociationMappings().add(am);
} // end of 'target class is in instance' section
}// end of iteration over target subclasses
} // end of 'not containment' section
} // end of 'target is EClass' section
} // end of ' not derived' section
} // end of iteration over EReferences
} // end of iteration over concrete subclasses
} // end of method
/**
* @param path path to this node
* @return name of the parent of this node
*/
private String parentNodeName(String path)
{
String parent = "node()"; // default if you cannot fix it
StringTokenizer st = new StringTokenizer(path,"/");
while (st.hasMoreTokens())
{
String step = st.nextToken();
if (st.hasMoreTokens()) parent = step;
}
return parent;
}
/**
*
* @param am an association mapping
* @param end1 one of its association end mappings
* @param end2 the other association end mapping
* Attach the two end mappings in the correct way,
* depending on the lexical order of the two role names
*/
private void addEnds(AssocMapping am, AssocEndMapping end1, AssocEndMapping end2)
{
String role1 = end1.getMappedRole();
String role2 = end2.getMappedRole();
if (role1.equals(ModelUtil.end1Role(role1, role2)))
{
am.setMappedEnd1(end1);
am.setMappedEnd2(end2);
}
else
{
am.setMappedEnd1(end2);
am.setMappedEnd2(end1);
}
}
/**
*
* @param child an ElementDef which has not yet been expanded
* @param altStructure a tree structure definition
* Expand the ElementDef by adding child nodes from the appropriate type
* @throws MapperException
*/
private void expandNode(ElementDef child,StructureDefinition altStructure)
throws MapperException
{
child.setExpanded(true);
ElementDef toCopy = altStructure.typeStructure(child.getType());
if (toCopy == null) throw new MapperException("Cannot find type '" + child.getType() + "'");
// two-stage copy to stop EMF blowing up
Vector<ElementDef> elDefs = new Vector<ElementDef>();
for (Iterator<ElementDef> ie = toCopy.getChildElements().iterator();ie.hasNext();)
elDefs.add(ie.next());
for (Iterator<ElementDef> ie= elDefs.iterator(); ie.hasNext();)
child.getChildElements().add(ie.next());
Vector<AttributeDef> aDefs = new Vector<AttributeDef>();
for (Iterator<AttributeDef> ia = toCopy.getAttributeDefs().iterator();ia.hasNext();)
aDefs.add(ia.next());
for (Iterator<AttributeDef> ia = aDefs.iterator();ia.hasNext();)
child.getAttributeDefs().add(ia.next());
}
/**
* When the association mappings were made in the first pass over the
* mapped structure, they were only made to one subset of each target class.
* For mappings of non-containment associations, if there is more than one subset
* of the 'long-range' class picked out by the cross-condition, then there has to be
* one association mapping for every subset of that class.
*
* This method also puts in the long-range paths for associations with cross-conditions,
* even for mappings with only one subset.
*
* @param anElement the element whose mappings are to be completed;and by recursive
* descent, all its descendant elements
*/
private void completeAssociationMappings(ElementDef anElement) throws MapperException
{
NodeMappingSet nms = anElement.getNodeMappingSet();
if (nms != null)
{
// collect the list of mappings before you start to modify it
Vector<AssocMapping> startMappings = new Vector<AssocMapping>();
for (Iterator<AssocMapping> ia = nms.getAssociationMappings().iterator();ia.hasNext();)
startMappings.add(ia.next());
for (Iterator<AssocMapping> ia = startMappings.iterator();ia.hasNext();)
{
AssocMapping am = ia.next();
String longRangeClassName = getLongRangeClassName(am);
if (longRangeClassName != null)
{
Vector<String> paths = mappingPaths.get(longRangeClassName);
if (paths == null) throw new MapperException("No mapping paths for class '" + longRangeClassName + "'");
// you need to put in long-range paths, even if the long-range class only one mapped subset
for (int p = 0; p < paths.size(); p++)
{
String subset = "";
if (p > 0) subset = "s" + p;
cloneAssociationMapping(nms,subset,am);
}
}
}
}
for (Iterator<ElementDef> ie = anElement.getChildElements().iterator(); ie.hasNext();)
completeAssociationMappings(ie.next());
}
/**
*
* @param am an association mapping
* @return if onde end of this association mapping has a cross condition,
* return the qualified name of the class picked out by that condition;
* otherwise return null
* @throws MapperException
*/
private String getLongRangeClassName(AssocMapping am) throws MapperException
{
String longRange = null;
// find the end with a cross-condition , if there is one
for (int e = 0; e< 2; e++)
{
AssocEndMapping end = am.getMappedEnd(e);
if (end.getCrossConditions().size() > 0)
longRange = ModelUtil.getQualifiedClassName(end.getMappedClass(), end.getMappedPackage());
}
return longRange;
}
/**
*
* @param nms a NodeMappingSet
* @param subset a subset string of a class mapping
* @param am and AssocMapping
* make a clone of the association mapping, which may differ only in the subset of
* the long-range class (picked out by a cross-condition).
* If this subset differs from the original ,add the mapping to the set.
* @throws MapperException
*/
private void cloneAssociationMapping(NodeMappingSet nms,String subset,AssocMapping am)
throws MapperException
{
boolean differentSubset = false;
AssocMapping clone = MapperFactory.eINSTANCE.createAssocMapping();
clone.setMultiWay(MultiWay.REDUNDANT);
for (int e = 0; e < 2; e++)
{
AssocEndMapping end = am.getMappedEnd(e);
AssocEndMapping cloneEnd = MapperFactory.eINSTANCE.createAssocEndMapping();
cloneEnd.setMappedClass(end.getMappedClass());
cloneEnd.setMappedPackage(end.getMappedPackage());
cloneEnd.setMappedRole(end.getMappedRole());
cloneEnd.setSubset(end.getSubset()); // may be reset below
if (end.getCrossConditions().size() > 0)
{
differentSubset = (!end.getSubset().equals(subset));
CrossCondition cc = end.getCrossConditions().get(0);
cloneEnd.setSubset(subset);
CrossCondition cloneCC = MapperFactory.eINSTANCE.createCrossCondition();
cloneCC.setLeftPath(cc.getLeftPath());
cloneCC.setRightPath(cc.getRightPath());
cloneCC.setTest(cc.getTest());
cloneEnd.getMappingConditions().add(cloneCC);
/* the long cross paths must be set using the correct subsets for each mapping,
* even for existing mappings if no new mapping is being added */
AssocEndMapping otherEnd = am.getMappedEnd(1-e);
if (!differentSubset) setLongPaths(end, otherEnd.getQualifiedClassName(),otherEnd.getSubset());
else setLongPaths(cloneEnd, otherEnd.getQualifiedClassName(),otherEnd.getSubset());
}
if (e == 0) clone.setMappedEnd1(cloneEnd);
else if (e == 1)clone.setMappedEnd2(cloneEnd);
}
// only add this association mapping if it has a target subset different from the original
if (differentSubset) nms.getAssociationMappings().add(clone);
}
/**
* @param aem an association end mapping, for the 'long' end that has a cross-condition
* @param qualifiedClassName the class name of the object whose object mapping is on the same node
* as this association (end)mapping
* @param subset the subset of the object whose object mapping is on the same node
* as this association (end)mapping
* Set up the cross-paths of the association end mapping to go up to the root and down
* to the appropriate node, so that if the default cross-path is too short, it is not taken
*/
private void setLongPaths(AssocEndMapping aem, String qualifiedClassName, String subset)
throws MapperException
{
/* object to association path; the down section of the path leads to the node
* of the object mapping which is the same node as this association mapping is on */
aem.setObjectToAssociationPath(makeCrossPath(qualifiedClassName, subset));
// association to object path; leads to the object mapping of the target class
aem.setAssociationToObjectPath(makeCrossPath(aem.getQualifiedClassName(), aem.getSubset()));
}
/**
* @param qualifiedClassName
* @param subset
* @return a cross path which goes up to the root and down to the set of
* nodes containing object mappings to this class and subset
*/
private String makeCrossPath(String qualifiedClassName, String subset)
throws MapperException
{
String downPath = "";
try{
Vector<String> paths = mappingPaths.get(qualifiedClassName);
// the path to subset "" is first in the Vector; then subsets "s1", "s2", etc.
if (subset.equals("")) downPath = paths.get(0);
else downPath = paths.get(new Integer(subset.substring(1)).intValue());
}
catch(Exception ex) {throw new MapperException("Failed to calculate cross path for class'"
+ qualifiedClassName + " ', subset '" + subset + "'; " + ex.getMessage());}
// strip off the first '/' of the path and add 'ancestor::' in stead
return ("ancestor::" + downPath.substring(1));
}
}