package com.openMap1.mapper.views;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.Map;
import java.util.Vector;
import org.eclipse.emf.common.util.EMap;
import org.eclipse.emf.ecore.EAttribute;
import org.eclipse.emf.ecore.EClass;
import org.eclipse.emf.ecore.EClassifier;
import org.eclipse.emf.ecore.EReference;
import org.eclipse.emf.ecore.EStructuralFeature;
import com.openMap1.mapper.actions.MakeITSMappingsAction;
import com.openMap1.mapper.core.MapperException;
import com.openMap1.mapper.mapping.AssociationMapping;
import com.openMap1.mapper.mapping.objectMapping;
import com.openMap1.mapper.query.DataSource;
import com.openMap1.mapper.reader.MDLXOReader;
import com.openMap1.mapper.reader.XOReader;
import com.openMap1.mapper.structures.ITSAssociation;
import com.openMap1.mapper.structures.ITSAttribute;
import com.openMap1.mapper.util.GenUtil;
import com.openMap1.mapper.util.ModelUtil;
import com.openMap1.mapper.ElementDef;
import com.openMap1.mapper.ImportMappingSet;
import com.openMap1.mapper.NodeDef;
/**
* Class denoting an EClass in the RMIM view and the other information
* needed to construct the RMIM Tree View.
* This information is:
* - its parent LabelledEClass
* - the association that reached it.
*
* The class is also used as a wrapper for EClasses more generally -
* first to avoid duplicating code that has a lot in common for
* EClasses and LabelledEClass, and second to put any useful methods in the wrapper.
* Uses a different constructor when not an RMIM class.
*/
public class LabelledEClass{
/**
* @return the LabelledEClass that is parent to this one
* in the RMIM tree diagram
*/
public LabelledEClass parent() {return parent;}
private LabelledEClass parent;
// see cached implementation getChildren below
private ArrayList<LabelledEClass> children;
/**
* @return the name of the association that reached this node of the RMIM tree
* from its parent node
*/
public String associationName() {return associationName;}
private String associationName;
/**
* @return the EClass that this is a wrapper for
*/
public EClass eClass() {return eClass;}
private EClass eClass;
private String subsetToMap = null;
/**
* @return the subset to use when making an object mapping to this class
*/
public String subsetToMap() {return subsetToMap;}
public void setSubsetToMap(String subsetToMap) {this.subsetToMap = subsetToMap;}
/**
* @return true if a mapping to this class is about to be made
* (i.e if subsetToMap has been set to a non-null value)
*/
public boolean makeMapping() {return (subsetToMap != null);}
public boolean isRMIMClass() {return isRMIMClass;}
private boolean isRMIMClass;
public boolean isMapped() {return (!objectMappingText.equals(""));}
/**
* @return text for the class model view describing the location of the object mapping
*/
public String getObjectMappingText() {return objectMappingText;}
private String objectMappingText = "";
public void setObjectMappingText(String text) {objectMappingText = text;}
/**
* @return the subset that has been used in an object mapping for this EClass
*/
public String getMappedSubset() {return mappedSubset;}
private String mappedSubset = null;
public void setMappedSubset(String subset) {mappedSubset = subset;}
//---------------------------------------------------------------------------------------
// constructor
//---------------------------------------------------------------------------------------
/**
* Constructor for RMIM classes, which have a position in an RMIM tree and a parent class
* @param eClass the EClass object of which this is a wrapper
* @param associationName the association that reached it
* @param parent the parent node in the RMIM tree
*/
public LabelledEClass(EClass eClass,String associationName,LabelledEClass parent)
{
this.eClass = eClass;
this.associationName = associationName;
this.parent = parent;
isRMIMClass = true;
}
/**
* Constructor for use as a wrapper for any EClass
* @param eClass
*/
public LabelledEClass(EClass eClass)
{
this.eClass = eClass;
this.associationName = null;
this.parent = null;
isRMIMClass = false;
}
/**
* @return the child LabelledEClass objects of this node
* cached implementation so it does not carry on creating new sets of children
* change 5/13 RPW so that any node reached by a non-containment relation has no child nodes
*/
public ArrayList<LabelledEClass> getChildren()
{
if (children == null)
{
children = new ArrayList<LabelledEClass>();
boolean showsSomeChildren = true;
if (parent() != null)
{
EStructuralFeature thisRef = parent().eClass().getEStructuralFeature(associationName);
showsSomeChildren = ((EReference)thisRef).isContainment();
}
if (showsSomeChildren) for (Iterator<EReference> it = eClass.getEReferences().iterator();it.hasNext();)
{
EReference ref = it.next();
String assocName = ref.getName();
EClassifier ec = ref.getEType();
if (ec instanceof EClass)
{
LabelledEClass child = new LabelledEClass((EClass)ec, assocName, this);
children.add(child);
}
}
}
return children;
}
/**
* @param refName
* @return the LabelledEClass child reached by the given association name,
* if there is one; null otherwise
*/
public LabelledEClass getNamedAssocChild(String refName)
{
LabelledEClass child = null;
for (Iterator<LabelledEClass> il = getChildren().iterator();il.hasNext();)
{
LabelledEClass candidate = il.next();
if (candidate.associationName.equals(refName)) child = candidate;
}
return child;
}
/**
* @param refName
* @param className
* @return the LabelledEClass child reached by the given association name, with the given class name,
* if there is one; null otherwise
*/
public LabelledEClass getNamedAssocAndClassChild(String refName, String className)
{
LabelledEClass child = null;
for (Iterator<LabelledEClass> il = getChildren().iterator();il.hasNext();)
{
LabelledEClass candidate = il.next();
if ((candidate.associationName.equals(refName)) && (candidate.eClass().getName().equals(className)))
child = candidate;
}
return child;
}
/**
*
* @return the EReference pointing from the parent eClass to this eClass
*/
public EReference getRefToThisClass()
{
EReference ref = null;
if (parent() != null)
{
for (Iterator<EStructuralFeature> it = parent().eClass().getEStructuralFeatures().iterator();it.hasNext();)
{
EStructuralFeature next = it.next();
if (next instanceof EReference)
{
EReference r = (EReference)next;
if ((r.getName().equals(associationName)) && (r.getEType().getName().equals(eClass().getName()))) ref = r;
}
}
}
return ref;
}
/**
* @return true if the class is now used in the micro-ITS, either because it has an EAttribute
* which is marked as included on the EAnnotation for the path to this LabelledEClass,
* or because it has an EReference which is marked as used (because there are EAttributes
* which are included, somewhere below the EReference)
*/
public boolean isActuallyUsedInMicroITS()
{
boolean inITS = false;
// check if any EAttribute is included in the micro-ITS
for (Iterator<EAttribute> ia = eClass.getEAllAttributes().iterator();ia.hasNext();)
{
EAttribute ea = ia.next();
String note = FeatureView.getMicroITSAnnotation(ea,getPath());
if (note != null) try
{
ITSAttribute itsa = new ITSAttribute(note);
if (itsa.isIncluded()) inITS = true;
}
catch (MapperException ex) {}
}
// check if any EReference is marked as used (not recursive)
for (Iterator<EReference> it = eClass.getEReferences().iterator();it.hasNext();)
{
EReference ref = it.next();
String note = FeatureView.getMicroITSAnnotation(ref,getPath());
if (note != null) try
{
ITSAssociation itsa = new ITSAssociation(note);
if (itsa.attsIncluded()) inITS = true;
}
catch (MapperException ex) {}
}
return inITS;
}
/**
* @return true if the class has been marked as used in the ITS,
* on the annotation of its parent association for the correct path.
*/
public boolean isMarkedUsedInMicroITS()
{
boolean marked = false;
if (parent() != null)
{
String parentPath = parent().getPath();
// find the EReference from the parent with the correct name
for (Iterator<EReference> ir = parent().eClass().getEReferences().iterator();ir.hasNext();)
{
EReference ref = ir.next();
if (ref.getName().equals(associationName())) try
{
// find the EAnnotation with the correct path
String note = FeatureView.getMicroITSAnnotation(ref,parentPath);
if (note != null)
{
ITSAssociation itsa = new ITSAssociation(note);
marked = itsa.attsIncluded();
}
}
catch (MapperException ex) {}
}
}
return marked;
}
/**
* mark the EReference leading to this EClass, with the appropriate path,
* as having descendant attributes included or not (if used is true or false)
* @param used
*/
public void markAsUsedInMicroITS(boolean used)
{
// If this is the top LabelledEClass, there is no EReference leading to it
if (parent() != null)
{
String parentPath = parent().getPath();
for (Iterator<EReference> ir = parent().eClass().getEReferences().iterator();ir.hasNext();)
{
EReference ref = ir.next();
if (ref.getName().equals(associationName())) try
{
String note = FeatureView.getMicroITSAnnotation(ref,parentPath);
if (used)
{
ITSAssociation itsa = new ITSAssociation();
if (note != null) itsa = new ITSAssociation(note);
itsa.setAttsIncluded(true);
FeatureView.addMicroITSAnnotation(ref, parentPath, itsa.stringForm());
}
else if ((!used) && (note != null))
{
FeatureView.removeMicroITSAnnotation(ref, parentPath);
}
}
catch (MapperException ex) {}
}
}
}
/* for all ancestor EAssociations, set or unset the used flag.
* Ascend the tree until you find a flag that is already set correctly,
* or you reach the top of the tree */
public void markWithAncestors(boolean used)
{
// mark the association from the parent leading to this class
markAsUsedInMicroITS(used);
// mark all ancestors until you come to one which is already correctly marked
LabelledEClass current = parent();
while (current != null)
{
boolean oldUsedState = current.isMarkedUsedInMicroITS();
boolean newUsedState = current.isActuallyUsedInMicroITS();
if (newUsedState != oldUsedState) // need to mark and ascend
{
current.markAsUsedInMicroITS(newUsedState);
current = current.parent(); // null if current was top of the tree
}
else if (newUsedState == oldUsedState) current = null; // need go no further
}
}
/**
* @return the path of association names to this EClass - with the class
* name of the top class at its start
*/
public String getPath()
{
if (associationName == null) return eClass.getName();
if (parent() == null) return eClass.getName(); // not sure how this occurs when associationName != null, but it does
return parent().getPath() + "/" + associationName;
}
/**
*
* @param className
* @return a descendant LabelledEClass with a given name, or null if there is none
* Note this could fail if there is an ancestor with the same name as a descendant.
*/
public LabelledEClass getDescendant(String className)
{
if (className.equals(eClass().getName())) return this;
// cut off infinite recursion, but crudely; this can cut it off if an ancestor name coincides
if (nameOccurrences() > 1) return null;
for (Iterator<LabelledEClass> it = getChildren().iterator(); it.hasNext();)
{
LabelledEClass child = it.next();
if (child.getDescendant(className) != null) return child.getDescendant(className);
}
return null;
}
/**
* @param className
* @return the number of times a class with a given name occurs in the path from the root class to this class
*/
public int countOccurrences(String className)
{
int occs = 0;
if (parent() != null) occs = parent().countOccurrences(className);
if (eClass().getName().equals(className)) occs++;
return occs;
}
/**
* @param className
* @return true if the path from the named ancestor class to this class consists only of 1..1 associations
*/
public boolean isOneToOneDescendant(String className)
{
if (className.equals(eClass().getName())) return true;
if (parent() == null) return false;
EReference fromParent = (EReference)parent().eClass().getEStructuralFeature(associationName);
if (fromParent.getLowerBound() == 0) return false;
if (fromParent.getUpperBound() == -1) return false;
return parent().isOneToOneDescendant(className);
}
/**
*
* @return the number of times this class name occurs in the path from the root class to this class
*/
public int nameOccurrences() {return countOccurrences(eClass().getName());}
/**
* @return true if this EClass is a single child of its parent class
*/
public boolean isSingleChild()
{
if (parent() != null)
{
EReference ref = (EReference)parent().eClass().getEStructuralFeature(associationName());
if ((ref != null) && (ref.getUpperBound() == 1)) return true;
}
return false;
}
//---------------------------------------------------------------------------------------------
// methods applicable for any wrapped EClass
//---------------------------------------------------------------------------------------------
/**
* @param dataSource a data source, which defines a top-level XOReader.
* If this is an RMIM class, it is assumed that the top ancestor of this LabelledEClass is represented
* in the top of the data source, and that this LabelledEClass
* is represented either in that XOReader or one that is imported by a chain of imports.
*
* @return the XOReader which represents this class and its associations and properties
* (i.e if this class is represented on an importing node, it returns the imported );
* or return null if there is any problem.
*/
public XOReader getLocalReader(DataSource dataSource) throws MapperException
{
XOReader reader = null;
if (!(dataSource.getReader() instanceof MDLXOReader))
{
if (dataSource.getReader().representsObject(ModelUtil.getQualifiedClassName(this.eClass())))
reader = dataSource.getReader();
}
else if (dataSource.getReader() instanceof MDLXOReader)
{
MDLXOReader mReader = (MDLXOReader)dataSource.getReader();
/* RMIM classes; avoid going over all imported mapping sets by going back
* through ancestors in the RMIM tree, assuming that the last
* ancestor is represented in the top mapping set */
if (isRMIMClass())
{
/* the end of the recursion; return the reader that represents this class. For RMIM classes
* this is expected always to be the top reader, so it will not search through imported readers*/
if (parent() == null)
{
if (mReader.representsObjectLocally(getQualifiedClassName()))
reader = dataSource.getReader();
}
/* recursive step; find the local reader of the parent object. If that represents this
* class on an importing node, return the imported XOReader */
else
{
MDLXOReader parentReader = (MDLXOReader)parent().getLocalReader(dataSource);
if ((parentReader != null) && (parentReader.representsObjectLocally(getQualifiedClassName())))
{
// find the Association Mappings with the correct parent class, role name and child class
Vector<AssociationMapping> amv = parentReader.getAssociationRoleMappingsLocal
(parent.getQualifiedClassName(), associationName, getQualifiedClassName());
if (amv.size() > 0) try // should be at most 1
{
AssociationMapping am = amv.get(0);
NodeDef nDef = am.mappedNode();
/* if the node of the association mapping (which is also expected to be
* the node representing the child object) imports a mapping set,
* return the XOReader for the mapping set; otherwise return the XOReader of the parent */
if (nDef instanceof ElementDef)
{
ImportMappingSet ims = ((ElementDef)nDef).getImportMappingSet();
if (ims != null) reader = parentReader.getImportedReader(ims);
else reader = parentReader;
}
}
catch (MapperException ex) {GenUtil.surprise(ex,"getLocalReader");}
}
}
}
/* non-RMIM classes; first find the top XOReader that represents the class, and then check
* if the node representing the class imports another mapping set. If so,
* return the imported mapping set */
else if (!isRMIMClass())
{
// get the top reader representing this class locally - which may not be the imported reader you want.
MDLXOReader topReader = (MDLXOReader)mReader.readerRepresentingObject(getQualifiedClassName());
if (topReader != null)
{
reader = topReader;
/* if any of the object mappings to the class in that mapping set are on a Node that imports,
* return the importing mapping set rather than the importing one. */
Vector<objectMapping> oMaps = topReader.objectMappingsByClassName(getQualifiedClassName());
for (Iterator<objectMapping> it = oMaps.iterator(); it.hasNext();)
{
NodeDef nDef = it.next().mappedNode();
if (nDef instanceof ElementDef) try
{
ImportMappingSet ims = ((ElementDef)nDef).getImportMappingSet();
if (ims != null) reader = topReader.getImportedReader(ims);
}
catch (MapperException ex) {GenUtil.surprise(ex,"getLocalReader");}
}
}
}
}
return reader;
}
public String getQualifiedClassName()
{
return ModelUtil.getQualifiedClassName(eClass());
}
public String getRIMorDataTypeClassName()
{
if (eClass().getEPackage().getName().equals("datatypes")) return eClass().getName();
return ModelUtil.getMIFAnnotation(eClass(),"RIM Class");
}
/**
* @return true if this class has a fixed value on any attribute
*/
public boolean hasSomeFixedValue() throws MapperException
{
boolean hasFixedValue = false;
for (Iterator<EStructuralFeature> it = this.eClass().getEStructuralFeatures().iterator();it.hasNext();)
{
EStructuralFeature feat = it.next();
if ((feat instanceof EAttribute) && (getAnnotatedFixedValue(feat.getName()) != null)) hasFixedValue = true;
}
return hasFixedValue;
}
/**
* get the fixed value of some attribute of the EClass, by comparing four values:
* (1) the value from a MIF annotation with key 'fixed value' on the EAttribute
* (2) the value from a MIF annotation like '<details key="constraint:code/@code" value="10160-0"/>' on the eClass or an ancestor
* (3) the value from a MIF annotation with key 'fixed att value' on the EReference to the class
* (4) the value from an annotation like '<details key="fixed:ClinicalDocument/messageType" value="POCD_MT010011GB01"/>'
* with an annotation source like "urn:hl7-org:v3/microITS/cda.mapper" which depends on the mapping set.
* If (4) exists, it takes priority over (1) or (2) or (3).
* If (1) or (2) or (3) give different values, throw an Exception.
* @param attName
* @return the fixed value as above, or null if there is none
* @throws MapperException if there is no EAttribute of this name, or if values (1) and (2) clash
*/
public String getAnnotatedFixedValue(String attName) throws MapperException
{
String fixedValue = null;
EStructuralFeature feat = eClass().getEStructuralFeature(attName);
if ((feat != null) && (feat instanceof EAttribute))
{
EAttribute att = (EAttribute) feat;
// get a MIF fixed value from the EAttribute
String fixedVal1 = ModelUtil.getEAnnotationDetail(att, "fixed value");
// get a MIF fixed value from the EClass or an ancestor EClass
String path = "@" + attName;
String fixedVal2 = getAncestorFixedValue(path);
// get a MIF fixed value from the EReference (NHS MIF files only)
String fixedVal3 = null;
if (getRefToThisClass() != null)
{
String fv = ModelUtil.getEAnnotationDetail(getRefToThisClass(), "fixed att value"); // may be null - no problem
String[][] classAtts = MakeITSMappingsAction.NHSFixedAttributes;
for (int i = 0; i < classAtts.length;i++)
{
String[] classAtt = classAtts[i];
if ((classAtt[0].equals(eClass.getName())) && (classAtt[1].equals(attName))) fixedVal3 = fv;
}
}
checkEquals(fixedVal1,fixedVal2,attName);
checkEquals(fixedVal1,fixedVal3,attName);
checkEquals(fixedVal2,fixedVal3,attName);
// take whichever of the three is not null
if (fixedVal1 != null) fixedValue = fixedVal1;
if (fixedVal2 != null) fixedValue = fixedVal2;
if (fixedVal3 != null) fixedValue = fixedVal3;
// specific fixed value requested for this path; takes precedence over the others
String attKey = "fixed:" + getPath();
String fixedVal4 = FeatureView.getMicroITSAnnotation(att, attKey);
if (fixedVal4 != null) fixedValue = fixedVal4;
}
else System.out.println("Class " + eClass.getName() + " has no attribute " + attName);
return fixedValue;
}
private void checkEquals(String fixedVal1,String fixedVal2,String attName) throws MapperException
{
// if the first two values are different, throw an Exception (should check all combinations)
if ((fixedVal1 != null) && (fixedVal2 != null) && (!fixedVal1.equals(fixedVal2)))
throw new MapperException ("Different annotations on EClass '" + eClass().getName()
+ "' for attribute '" + attName + "' give different fixed values '"
+ fixedVal2 + "' and '" + fixedVal1 + "'");
}
/**
* get the fixed value of an attribute from an annotation on the EClass or an ancestor, with a path to the attribute
* @param path
* @return
*/
public String getAncestorFixedValue(String path)
{
String value = ModelUtil.getMIFAnnotation(eClass(), "constraint:" + path);
if ((value == null) && (parent() != null))
{
String parentPath = associationName + "/" + path;
value = parent().getAncestorFixedValue(parentPath);
}
return value;
}
/**
* @param path
* @return true if the MIF annotations define any
* fixed values for this class or any in its subtree
*/
public boolean hasMIFFixedValuesInSubtree()
{
return hasMIFFixedValuesInSubtree("");
}
/**
* @param path
* @return when path = "", return true if the MIF annotations define any
* fixed values for this class or any in its subtree
*/
boolean hasMIFFixedValuesInSubtree(String path)
{
boolean hasFixedValues = false;
// find if any MIF annotations on this class define a fixed value starting with the required path
String detailPrefix = "constraint:" + path;
EMap<String,String> details = ModelUtil.getMIFAnnotationDetails(eClass());
for (Iterator<Map.Entry<String, String>> it = details.entrySet().iterator(); it.hasNext();)
{
Map.Entry<String, String> ent = it.next();
if (ent.getKey().startsWith(detailPrefix)) hasFixedValues = true;
}
// if not, check the ancestors
if ((!hasFixedValues) && (parent() != null))
{
String parentPath = associationName + "/" + path;
hasFixedValues = parent().hasMIFFixedValuesInSubtree(parentPath);
}
return hasFixedValues;
}
}