package com.openMap1.mapper.actions;
import java.util.Enumeration;
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.emf.common.util.BasicEList;
import org.eclipse.emf.common.util.EList;
import org.eclipse.emf.common.util.URI;
import org.eclipse.emf.ecore.EAnnotation;
import org.eclipse.emf.ecore.EAttribute;
import org.eclipse.emf.ecore.EClass;
import org.eclipse.emf.ecore.EModelElement;
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.EcoreFactory;
import org.eclipse.emf.ecore.EcorePackage;
import org.eclipse.jface.action.Action;
import org.eclipse.jface.action.IAction;
import com.openMap1.mapper.AssocEndMapping;
import com.openMap1.mapper.AssocMapping;
import com.openMap1.mapper.AttributeDef;
import com.openMap1.mapper.ElementDef;
import com.openMap1.mapper.MappedStructure;
import com.openMap1.mapper.MapperFactory;
import com.openMap1.mapper.MapperPackage;
import com.openMap1.mapper.MappingCondition;
import com.openMap1.mapper.MaxMult;
import com.openMap1.mapper.MinMult;
import com.openMap1.mapper.Namespace;
import com.openMap1.mapper.NodeDef;
import com.openMap1.mapper.NodeMappingSet;
import com.openMap1.mapper.ObjMapping;
import com.openMap1.mapper.PropMapping;
import com.openMap1.mapper.ValueCondition;
import com.openMap1.mapper.converters.CDAConverter;
import com.openMap1.mapper.converters.NHS_CDA_Wrapper;
import com.openMap1.mapper.core.MapperException;
import com.openMap1.mapper.impl.ElementDefImpl;
import com.openMap1.mapper.presentation.MapperEditor;
import com.openMap1.mapper.structures.ITSAssociation;
import com.openMap1.mapper.structures.ITSAttribute;
import com.openMap1.mapper.structures.MappableAssociation;
import com.openMap1.mapper.structures.StructureDefinition;
import com.openMap1.mapper.userConverters.NHS_CDA_TagRuleInterpreter;
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.AssociationView;
import com.openMap1.mapper.views.AttributeView;
import com.openMap1.mapper.views.ClassModelView;
import com.openMap1.mapper.views.FeatureView;
import com.openMap1.mapper.views.LabelledEClass;
import com.openMap1.mapper.views.WorkBenchUtil;
/**
* @author Robert
*
*/
public class MakeITSMappingsAction extends Action implements IAction{
boolean tracing = true;
private MapperEditor mapperEditor;
private MappedStructure mappedStructure;
private String mappingSetURIString;
private ClassModelView classModelView;
// wrapper class and package as defined in a mapping set - e.g 'com.OpenMap1.mapper.converters.NHS_CDA_Converter'
private String fullMappingsWrapperClassName = "";
/* wrapper class for simple mappings to CDA. The only role of this wrapper is to substitute keys for
* rendered html subtrees, so those subtrees are passed through a translation unchanged */
private String simpleMappingsWrapperClassName = "com.openMap1.mapper.converters.NHS_Simplified_CDA";
/** the package which is the root of the model */
public EPackage ecoreRoot() {return ecoreRoot;}
private EPackage ecoreRoot;
private String CDATagFileName = "CDATagNames.csv";
public boolean isNHSMIF()
{
if (ecoreRoot != null) return (ModelUtil.getMIFAnnotation(ecoreRoot, "isNHSMIF") != null);
return false;
}
public String messageName()
{
if (ecoreRoot != null) return (ModelUtil.getMIFAnnotation(ecoreRoot, "message"));
return null;
}
// names of the wrapper classes used for NHS-style CDAs and V3 messages
public static String NHSV3WrapperClass = "com.openMap1.mapper.converters.NHS_V3_Converter";
public static String NHSCDATemplatedWrapperClass = "com.openMap1.mapper.converters.NHS_CDA_Converter";
public static String NHSCDAWireWrapperClass = "com.openMap1.mapper.converters.NHS_CDA_Wire2";
// value of attribute npfitlc:contentId/@root
public static String NHSContentIdRoot = "2.16.840.1.113883.2.1.3.2.4.18.16";
public static String[][] NHSFixedAttributes = {{"II","extension"},{"CV","code"},{"CD","code"},{"CS","code"},{"BL","value"}};
// name of the wrapper class for other CDAs
public static String CDAWrapperClass = "com.openMap1.mapper.converters.CDAConverter";
/* if true, suppress fixed value mappings for the text attributes
* (representation, mediaType and partType) on name parts and address parts,
* because many example instances don't seem to have them
* (are they added by schema validation?) */
private boolean suppressFixedTextMappings = true;
private String[] suppressableAttribute = {"representation","mediaType","partType"};
private int warningState;
private int WARNINGS_ASK_USER = 1; // on the first warning message, ask if the user wants more
private int WARNINGS_SHOW_ALL = 2; // the user has asked to show all warning messages
private int WARNINGS_SUPPRESS = 3; // the user has asked to suppress warning messages
// used as a prefix in the description property of mapping set ElementDef nodes, to convey the wire-form CDA name for the schema
public static String CDA_NAME = "CDA name:";
// used as a prefix in the description property of mapping set AttributeDef nodes, to convey fixed values for the schema
public static String FIXED = "fixed:";
/* Outer key = class name in simple class model (the class may appear in many LabelleEClasses)
* Inner key = LabelledEClass path, from the root of simple class model to the parent of that class
* Value = subset for that occurrence of the class (usually "") */
private Hashtable<String,Hashtable<String,String>> subsetsForPaths;
// true if the simplified class model is to have multiple packages, one per template or section
private boolean multiPackages = true;
/* key = name for the template, as in an annotation on the full class model;
* value = template id, as in th package name in the full class model
*/
private Hashtable<String,String> TemplatePackageIds = new Hashtable<String,String>();
//----------------------------------------------------------------------------------------------------------
// Constructor
//----------------------------------------------------------------------------------------------------------
public MakeITSMappingsAction()
{
super("Make Simplified Model and Mappings");
}
/**
* Save the simplified Ecore class model, and mappings of the simplified
* and full message structures onto it
*/
public void run()
{
warningState = WARNINGS_ASK_USER;
classModelView = WorkBenchUtil.getClassModelView(false);
if (classModelView != null) try
{
ecoreRoot = classModelView.ecoreRoot();
mappingSetURIString = classModelView.mappingSetURI().toString();
mapperEditor = WorkBenchUtil.getMapperEditor(mappingSetURIString);
if (mapperEditor != null)
{
mappedStructure = WorkBenchUtil.mappingRoot(mapperEditor);
// check the Ecore model has some attributes included for this mapping set
if (hasIncludedAttributes(ecoreRoot))
{
// complete the annotations (for NHS fixed values, and text nodes) , and save the ecore model
LabelledEClass entryClass = classModelView.topLabelledEClass();
completeAnnotations(entryClass);
saveEcoreModel();
/* Create a simplified Ecore class model and two mapping sets -
* for the simple message and the full message mapped onto it.
* ( offer the user an option to constrain the simple message by a schema - no longer used) */
/* String prompt = "Optionally select Mapping Set to constrain simple message structure";
masterStructure = askUserForMappedStructure(mapperEditor, prompt); */
MappedStructure masterStructure = null;
makeSimpleModelAndMappings(masterStructure);
WorkBenchUtil.showMessage("Completed","Made simplified message, model, and mappings");
}
else WorkBenchUtil.showMessage("Error", "The RMIM class model has no attributes annotated to be included in an ITS for this mapping set");
}
else WorkBenchUtil.showMessage("Error", "No Mapper Editor found");
}
catch (Exception ex)
{
WorkBenchUtil.showMessage("Error",ex.getMessage());
ex.printStackTrace();
}
}
/**
* @param model an ECore model
* @return true if the model has any EAttributes annotated to be included in
* the mapping set currently active in the editor
* (does not check the detail of the annotations; they could conceivably not include
* any of the attributes, but this would be perverse)
*/
private boolean hasIncludedAttributes(EPackage model)
{
boolean hasIncluded = false;
for (Iterator<EClass> ic = ModelUtil.getAllClasses(model).iterator();ic.hasNext();)
if (hasAnySimplificationAnnotations(ic.next())) hasIncluded = true;
return hasIncluded;
}
/**
*
* @param theClass
* @return true if this EClass has any simplification annotations on its EReferences or EAttributes
*/
private boolean hasAnySimplificationAnnotations(EClass theClass)
{
boolean hasAnnotations = false;
for (Iterator<EStructuralFeature> ia = theClass.getEStructuralFeatures().iterator();ia.hasNext();)
{
EStructuralFeature ea = ia.next();
EAnnotation ann = ea.getEAnnotation(FeatureView.microITSURI());
if (ann != null)
{
hasAnnotations = true;
}
}
return hasAnnotations;
}
/**
*
* @param theClass
* @return true if this LabelledEClass has any simplification annotations on its EReferences or EAttributes,
* for the specific path to the LabelledEClass
*/
private boolean hasSimplificationAnnotations(LabelledEClass theClass)
{
boolean hasAnnotations = false;
for (Iterator<EStructuralFeature> ia = theClass.eClass().getEStructuralFeatures().iterator();ia.hasNext();)
{
EStructuralFeature ea = ia.next();
EAnnotation ann = ea.getEAnnotation(FeatureView.microITSURI());
if (ann != null)
{
String value = ann.getDetails().get(theClass.getPath());
if (value != null) hasAnnotations = true;
}
}
return hasAnnotations;
}
/**
* recursive descent of the LabelleEClass tree, as far as there are simplification annotations,
* completing the simplification annotations:
*
* (a) where NHS RMIM defines fixed values on II or CV or BL data types,
* which have given 'fixed att value' annotations in the Ecore model, add the simplification annotations
*
* (b) retain text subtrees, for any text node whose parent has annotations
*
* @param theClass
*/
private void completeAnnotations(LabelledEClass theClass) throws MapperException
{
// mark text nodes to be retained, if their parent node has simplification annotations
if ((theClass.associationName() != null) && (theClass.associationName().equals("text")))
{
// add a 'textContent' EAttribute to the class if it does not yet exist
SimplificationsFromExamplesAction.addTextContentAttribute(theClass.eClass());
SimplificationsFromExamplesAction.markAttributeUsedOrFixed(theClass,"textContent", "a", "a#",false);
}
if (hasSimplificationAnnotations(theClass))
{
// marking these attributes should be idempotent; does not matter if you do it many times
if (isNHSMIF()) SimplificationsFromExamplesAction.mark_CS_CV_CD_II_BL_Attributes(theClass);
// recursive descent - one more step if this class has annotations
for (Iterator<LabelledEClass> it = theClass.getChildren().iterator();it.hasNext();)
completeAnnotations(it.next());
}
}
private static String MADE_FROM_ITS = "Made from ITS";
/* check that this mapping set is either new (no structure at all)
* or has been made from an ITS.*/
public static boolean isMadeFromITS(MappedStructure mappedStructure)
{
// if there is no mapped structure to overwrite, return true
if (mappedStructure.getRootElement() == null) return true;
// if there is a mapped structure, return true if it was made from an ITS
return (mappedStructure.getRootElement().getDescription().equals(MADE_FROM_ITS));
}
//-----------------------------------------------------------------------------------------------------
// adding mappings to the simplified and full mapping set
//-----------------------------------------------------------------------------------------------------
/**
* set the multiplicities of a child ElementDef according to the
* cardinalities of the EReference
* @param next
* @param ref
*/
private void setMultiplicities(ElementDef next,EReference ref,LabelledEClass fullClass)
{
next.setMinMultiplicity(MinMult.ONE);
if (AssociationView.getConstrainedLowerBound(ref, fullClass) == 0) next.setMinMultiplicity(MinMult.ZERO);
next.setMaxMultiplicity(MaxMult.ONE);
if (AssociationView.getConstrainedUpperBound(ref, fullClass) == -1) next.setMaxMultiplicity(MaxMult.UNBOUNDED);
}
/**
* add an object mapping on an ElementDef;
* if there is an association from a parent class, add the association mapping
* @param node node on which to add mappings
* @param lClass the LabelledEClass to be mapped
* @param parentRef EReference from the parent class (null if there is none)
* @param parentSubset subset of the parent class object mapping (null if there is none)
* @param thisSubset the subset to use for this class mapping (if null, make one up)
* @return the new class mapping
*/
//** (not implemented) if lClass has a value condition annotation, add value conditions to the mappings in the full mapping set only
private ObjMapping makeObjectAndAssocMapping(ElementDef node,LabelledEClass lClass,
EReference parentRef, LabelledEClass parent)
{
String parentSubset = null;
if (parent != null) parentSubset = getSubset(parent.eClass().getName(),parent.parent());
String thisSubset = getSubset(lClass.eClass().getName(),lClass.parent());
// add a node mapping set if it has not been added already
NodeMappingSet nodeMaps = node.getNodeMappingSet();
if (nodeMaps == null)
{
nodeMaps = MapperFactory.eINSTANCE.createNodeMappingSet();
node.setNodeMappingSet(nodeMaps);
}
// make and add an object mapping
ObjMapping om = MapperFactory.eINSTANCE.createObjMapping();
om.setMappedClass(lClass.eClass().getName());
om.setMappedPackage(lClass.eClass().getEPackage().getName());
String qualifiedClassName = ModelUtil.getQualifiedClassName(lClass.eClass());
String subset = thisSubset;
if (subset == null) subset = AddMapperEditorActions.nextSubset(mappedStructure, qualifiedClassName);
om.setSubset(subset);
nodeMaps.getObjectMappings().add(om);
// add an association mapping, if there is a parent object mapping
if (parentSubset != null)
{
MappableAssociation mass = new MappableAssociation(lClass.parent().eClass(),parentSubset,
lClass.eClass(),subset,parentRef,true);
AssocMapping am = MapperFactory.eINSTANCE.createAssocMapping();
for (int end = 1; end < 3; end ++)
{
AssocEndMapping aem = MapperFactory.eINSTANCE.createAssocEndMapping();
aem.setMappedRole(mass.roleName(end));
aem.setMappedClass(mass.endClass(end).getName());
aem.setMappedPackage(mass.endClass(end).getEPackage().getName());
aem.setSubset(mass.getSubset(end));
aem.setRequiredForObject(mass.requiredForEnd(end));
if (end == 1) am.setMappedEnd1(aem);
if (end == 2) am.setMappedEnd2(aem);
}
nodeMaps.getAssociationMappings().add(am);
}
return om;
}
/**
* add a property mapping on the AttributeDef node
* @param nDef
* @param eClass
* @param subset subset of the object mapping, to match
* @param ea
*/
private void addPropertyMapping(NodeDef nDef,LabelledEClass lClass,EAttribute ea)
{
EClass eClass = lClass.eClass();
String subset = getSubset(eClass.getName(),lClass.parent());
// add a node mapping set to hold the mapping
NodeMappingSet nodeMaps = nDef.getNodeMappingSet();
if (nodeMaps == null)
{
nodeMaps = MapperFactory.eINSTANCE.createNodeMappingSet();
nDef.setNodeMappingSet(nodeMaps);
}
// create and add the property mapping
PropMapping pm = MapperFactory.eINSTANCE.createPropMapping();
pm.setMappedClass(eClass.getName());
pm.setMappedPackage(eClass.getEPackage().getName());
pm.setMappedProperty(ea.getName());
pm.setSubset(subset);
nodeMaps.getPropertyMappings().add(pm);
}
//-----------------------------------------------------------------------------------------------------------------
// Make a simplified class model, and two mapped structures, for the simple and full
// message structures mapped to it.
//-----------------------------------------------------------------------------------------------------------------
// the top package of the simplified class model, which all classes are in It is called com.OpenMap1.cda
private EPackage topPackage;
private Hashtable<String,EClass> simpleClasses;
// the created mapping set of the simple message structure onto the simple Ecore class model
private MappedStructure simpleMappings;
// the created mapping set of the full V3 message structure onto the simple Ecore class model
private MappedStructure fullMappings;
/* to keep track of which attribute in the simple mapping set corresponds to each attribute in
* the full mapping set. Used only for NHS RMIMs, but set up in all cases.
* Key = node in full mapping set; value = node in simple mapping set */
private Hashtable<NodeDef,NodeDef> matchedNodes;
/* used to write out a csv file of path-to-path attribute mappings between the simple and full structures */
private Vector<String[]> attributeMappings;
// header row for the csv file of attribute mappings
private String[] attributeMappingsHeader = {"Green CDA XPath","Templated full CDA XPath","Normative full CDA XPath","Data Type","Comments"};
/* used to write out a file of warnings found when making the simple class model and mappings */
private Vector<String[]> warnings;
// header row for the csv file of attribute mappings
private String[] warningsHeader = {"Path","Node","Warning"};
/**
* ask the user for a mapped structure (typically attached to a schema)
* to constrain the simple message structure
* @return the mapped structure
* @throws MapperException
*/
private MappedStructure askUserForMappedStructure(MapperEditor mapperEditor, String prompt) throws MapperException
{
MappedStructure ms = null;
String[] exts = {"*.mapper"};
String mapperFileLocation = FileUtil.getFilePathFromUser(mapperEditor,exts,prompt,false);
if (!mapperFileLocation.equals(""))try {ms = FileUtil.getMappedStructure(mapperFileLocation);}
catch (Exception ex) {throw new MapperException(ex.getMessage());}
return ms;
}
private void makeSimpleModelAndMappings(MappedStructure masterStructure) throws MapperException
{
// empty things
simpleClasses = new Hashtable<String,EClass>();
subsetsForPaths = new Hashtable<String,Hashtable<String,String>>();
matchedNodes = new Hashtable<NodeDef,NodeDef>();
attributeMappings = new Vector<String[]>();
attributeMappings.add(attributeMappingsHeader);
warnings = new Vector<String[]>();
warnings.add(warningsHeader);
// make the empty simple class model and two mapping sets.
makeInitialModelAndMappings();
// make the top class of the simple class model
LabelledEClass topFullClass = classModelView.topLabelledEClass();
String topClassName = topFullClass.eClass().getName();
LabelledEClass topSimpleClass = findOrMakeClass(getSimplePackageName(topFullClass.eClass()),topClassName,null,null);
// ensure this class will be the root of the tree in the class model view
ModelUtil.addMIFAnnotation(topSimpleClass.eClass(), "entry", "true");
// make the top node of the simple mapping set, and map the top class to it
ElementDef topSimpleNode = MapperFactory.eINSTANCE.createElementDef();
topSimpleNode.setName(topClassName);
topSimpleNode.setExpanded(true);
simpleMappings.setRootElement(topSimpleNode);
simpleMappings.setTopElementName(topClassName);
makeObjectAndAssocMapping(topSimpleNode,topSimpleClass,null,null);
// make the top node of the full mapping set, and map the top class to it
ElementDef topFullNode = MapperFactory.eINSTANCE.createElementDef();
topFullNode.setName(topClassName);
// for MDHT templated subclasses of ClinicalDocument, name the top node 'ClinicalDocument'
if (isClinicalDocumentClass(topFullClass)) topFullNode.setName("ClinicalDocument");
topFullNode.setExpanded(true);
fullMappings.setRootElement(topFullNode);
fullMappings.setTopElementName(topClassName);
ObjMapping topObjectMapping = makeObjectAndAssocMapping(topFullNode,topSimpleClass,null,null);
addFixedValueConditions(topObjectMapping, topFullClass,null);
// recurse down the full class model
fillMappingNodes(topFullClass,topSimpleClass,topFullNode,topSimpleNode, false, false);
addNodesForFixedValues(topFullNode,topFullClass);
// possibly constrain the simple mapped structure to be a subset of an existing structure
if (masterStructure != null) topSimpleNode = constrainSimpleStructure(topSimpleNode,masterStructure);
/* for NHS CDA, alter the full mapping set to have correct CDA order, note CDA tag names
* and mark both mapping sets with data types */
if (isNHSMIF())
{
orderFullMappingSet();
markMappingSetsWithTypes();
}
// add annotations to make javadoc for EMG generated java classes
addJavaDocAnnotations(fullMappings,topPackage);
// save the final state of the simple class model and the two mapping sets
FileUtil.saveResource(topPackage.eResource());
FileUtil.saveResource(simpleMappings.eResource());
FileUtil.saveResource(fullMappings.eResource());
// make and save a thin mapping set (full mapping set without all its fixed value conditions)
makeThinMappingSet();
// save the csv file of attribute mappings
String csvMappingsLocation = changeExtension(addSuffixToFileName(mappingSetURIString, "mappings"),"csv");
IFile csvMappingsFile = EclipseFileUtil.getFile(csvMappingsLocation);
EclipseFileUtil.writeCSVFile(finalMappingRows(attributeMappings), csvMappingsFile);
// save the csv file of warnings
String csvWarningsLocation = changeExtension(addSuffixToFileName(mappingSetURIString, "warnings"),"csv");
IFile csvWarningsFile = EclipseFileUtil.getFile(csvWarningsLocation);
EclipseFileUtil.writeCSVFile(warnings, csvWarningsFile);
}
/**
*
* @param labelledClass and entry class of an RMIM class model
* @return true if this class mentions the CDA wrapper class in its annotations
*/
private boolean isClinicalDocumentClass(LabelledEClass labelledClass)
{
boolean isClinicalDoc = false;
String wrapperClass = ModelUtil.getMIFAnnotation(labelledClass.eClass(), "wrapperClass");
if ((wrapperClass != null) && (wrapperClass.equals(CDAWrapperClass))) isClinicalDoc = true;
return isClinicalDoc;
}
/**
* add a row to the csv file of warnings, and sometimes give a warning to the user - if he
* has not asked to suppress warnings
* @param path
* @param att
* @param message message to be put in the row of the warnings csv file
* @param warningMessage message to be shown interactively
*/
private void warnAndAddWarningRow(String path, String att, String message, String warningMessage)
{
// show a warning if the user has not decided to suppress warnings
if (warningState != WARNINGS_SUPPRESS)
{
WorkBenchUtil.showMessage("Warning",warningMessage);
// if the user has not yet been asked whether to suppress warnings, ask once
if (warningState == WARNINGS_ASK_USER)
{
boolean suppress = WorkBenchUtil.askConfirm("Future Warning Messages", "Suppress further warning messages (which can be found in the Warnings csv file) ?");
if (suppress) warningState = WARNINGS_SUPPRESS;
else warningState = WARNINGS_SHOW_ALL;
}
}
// always add a row to the csv file of warnings
String [] warningRow = new String[3];
warningRow[0] = path;
warningRow[1] = att;
warningRow[2] = message;
warnings.add(warningRow);
}
/*
* Setting the grouping node in the simple structure and the object mapping in the full structure
* as side-effects of method makeNextGroupingClass does not work.
* So I have had to make nextGroupingNode an instance variable of the class,
* so it can set them.
*/
private ElementDef nextGroupingNode;
/**
* object mappings in the full mapping set have some fixed value conditions.
* Ensure that the full mapping set has the AttributeDef nodes to carry those values, and
* that the descriptions of those nodes say what the fixed values are.
* This method does recursion down the mapped structure, finding the corresponding LabelledEClass
* @param fullNode
* @param fullClass
*/
private void addNodesForFixedValues(ElementDef fullNode,LabelledEClass fullClass) throws MapperException
{
addNodesForFixedValuesOnOneElement(fullNode, fullClass);
for (Iterator<ElementDef> it = fullNode.getChildElements().iterator();it.hasNext();)
{
ElementDef nextFullNode = it.next();
String fullClassName = removePrefix(nextFullNode.getName());
LabelledEClass nextFullClass = fullClass.getNamedAssocChild(fullClassName);
if (nextFullClass == null) WorkBenchUtil.showMessage("Error in source class model","Cannot find association in source class model for ElementDef '" + nextFullNode.getName() + "'");
addNodesForFixedValues(nextFullNode,nextFullClass);
}
}
private String removePrefix(String nodeName)
{
String name = "";
StringTokenizer st = new StringTokenizer(nodeName,":");
while (st.hasMoreTokens()) name = st.nextToken();
return name;
}
/**
* adding nodes to the full mapping set for fixed values.
* This method iterates over all fixed values on mappings on one ElementDef
* @param fullNode
* @param fullClass
*/
private void addNodesForFixedValuesOnOneElement(ElementDef fullNode,LabelledEClass fullClass) throws MapperException
{
NodeMappingSet nms = fullNode.getNodeMappingSet();
if (nms != null) for (Iterator<ObjMapping> it = nms.getObjectMappings().iterator();it.hasNext();)
{
ObjMapping objMapping = it.next();
for (Iterator<MappingCondition> iu = objMapping.getMappingConditions().iterator();iu.hasNext();)
{
MappingCondition mappingCondition = iu.next();
if (mappingCondition instanceof ValueCondition)
{
ValueCondition valueCondition = (ValueCondition)mappingCondition;
String path = valueCondition.getLeftPath();
String value = valueCondition.getRightValue();
addNodesForOneFixedValue(fullNode,fullClass,path, value);
}
}
}
}
/**
* recursive descent of the path in one fixed value on an ObjMapping,
* adding ElementDefs and AttributeDefs where necessary, and noting the
* fixed value on the AttributeDef
* @param fullNode
* @param fullClass
* @param path
* @param value
*/
private void addNodesForOneFixedValue(ElementDef fullNode,LabelledEClass fullClass,String path, String value) throws MapperException
{
String step = firstNode(path);
// end of the path; add an attributeDef if needed, and note its value
if (step.startsWith("@"))
{
String attName = step.substring(1);
AttributeDef attDef = fullNode.getNamedAttribute(attName);
if (attDef == null)
{
attDef = MapperFactory.eINSTANCE.createAttributeDef();
attDef.setName(attName);
attDef.setMinMultiplicity(MinMult.ONE);
fullNode.getAttributeDefs().add(attDef);
}
attDef.setDescription(FIXED + value);
}
// an ElementDef along the path; find or make the ElementDef, and recurse
else
{
ElementDef childEl = fullNode.getNamedChildElement(step);
if (childEl == null)
{
childEl = MapperFactory.eINSTANCE.createElementDef();
childEl.setName(putInCorrectNamespace(step));
childEl.setMinMultiplicity(MinMult.ONE);
childEl.setMaxMultiplicity(MaxMult.ONE);
addAtCorrectPosition(fullNode,childEl,fullClass);
}
LabelledEClass fullChildClass = fullClass.getNamedAssocChild(removePrefix(step));
String newPath = restOfPath(path);
if (newPath.equals("")) throw new MapperException("Fixed value condition on element '" + step + "': value = " + value);
addNodesForOneFixedValue(childEl,fullChildClass,newPath,value);
}
}
/**
* Use the list of ERefences of the class in the full class model to find the correct position in
* the childElemenDefs of an ElementDef, to put a new child; and put it there
* @param fullNode
* @param childEl
* @param fullClass
* @throws MapperException
*/
private void addAtCorrectPosition(ElementDef fullNode,ElementDef childEl, LabelledEClass fullClass) throws MapperException
{
EList<EReference> refList = fullClass.eClass().getEAllReferences();
int correctPosition = 0;
// increase the target position by 1 for every ElementDef found before the new element in the list of associations of the class
boolean foundInRefList = false;
for (Iterator<EReference> it = refList.iterator();it.hasNext();)
{
String refName = it.next().getName();
if (refName.equals(removePrefix(childEl.getName()))) foundInRefList = true;
if ((fullNode.getNamedChildElement(refName) != null) && (!foundInRefList)) correctPosition++;
}
if (!foundInRefList) throw new MapperException("Found no correct position for new child element '" + childEl.getName() + "'");
// add the newElementDef at the end of the list, then move it to the right position
EList<ElementDef> children = fullNode.getChildElements();
children.add(childEl);
children.move(correctPosition, childEl);
}
/**
* name of the first step in a path
* @param path
* @return
*/
private String firstNode(String path)
{
StringTokenizer steps = new StringTokenizer(path,"/");
return steps.nextToken();
}
/**
* remainder of a path, when the first step has been taken off
* @param path
* @return
*/
private String restOfPath(String path)
{
StringTokenizer steps = new StringTokenizer(path,"/");
steps.nextToken();
String rest = "";
while (steps.hasMoreTokens())
{
rest = rest + steps.nextToken();
if (steps.hasMoreTokens()) rest = rest + "/";
}
return rest;
}
/**
* recurse down the full class model, making the simple class model and the
* simple and full message mapping sets
* @param fullClass current class in the full class model (drives the recursion)
* @param simpleClass current class in the simple class model
* @param fullNode current node in the full mapping set
* @param simpleNode current node in the simple mapping set
* @param zeroLowerBound true if some association with lower bound 0
* has been encountered since the last simple class
* @throws MapperException
*/
private void fillMappingNodes(LabelledEClass fullClass,LabelledEClass simpleClass,
ElementDef fullNode,ElementDef simpleNode, boolean zeroLowerBound,boolean ordered)
throws MapperException
{
for (Iterator<EAttribute> ia = fullClass.eClass().getEAllAttributes().iterator();ia.hasNext();)
{
EAttribute att = ia.next();
fillAttributeNode(att,fullClass,simpleClass,fullNode,simpleNode,zeroLowerBound);
}
for (Iterator<EReference> ir = fullClass.eClass().getEAllReferences().iterator();ir.hasNext();)
{
EReference ref = ir.next();
fillAssociationNode(ref,fullClass,simpleClass,fullNode,simpleNode,zeroLowerBound,ordered);
}
}
/**
* add a position attributeDef to an ElementDef - which may be in the simple or full mapping set
* @param elDef
*/
private AttributeDef addPositionAttributeDef(ElementDef elDef)
{
AttributeDef posAtt = MapperFactory.eINSTANCE.createAttributeDef();
posAtt.setName(ElementDefImpl.ELEMENT_POSITION_ATTRIBUTE);
elDef.getAttributeDefs().add(posAtt);
return posAtt;
}
public static String POSITION_PROPERTY_NAME = "order_property";
/**
* add a position EAttribute to a class in the simple class model, to be mapped to the position attributes
* in simple and full mapping sets
* @param theClass
*/
private EAttribute addPositionEAttribute(LabelledEClass theClass)
{
EStructuralFeature feat = theClass.eClass().getEStructuralFeature(POSITION_PROPERTY_NAME);
if ((feat != null) && (feat instanceof EAttribute)) return (EAttribute)feat;
else
{
EAttribute pos = EcoreFactory.eINSTANCE.createEAttribute();
pos.setName(POSITION_PROPERTY_NAME);
pos.setEType(EcorePackage.eINSTANCE.getEInt());
theClass.eClass().getEStructuralFeatures().add(pos);
return pos;
}
}
/**
* @param ref
* @param fullClass
* @param simpleClass
* @param fullNode
* @param simpleNode
* @param zeroLowerBound
* @param ordered if the parent node required its child nodes to be ordered
* @throws MapperException
*/
private void fillAssociationNode(EReference ref,LabelledEClass fullClass,LabelledEClass simpleClass,
ElementDef fullNode,ElementDef simpleNode, boolean zeroLowerBound, boolean ordered)
throws MapperException
{
String sourceClassName = fullClass.eClass().getName();
LabelledEClass nextFullClass = fullClass.getNamedAssocChild(ref.getName());
ITSAssociation itsa = FeatureView.getITSAssociation(ref, fullClass.getPath());
if ((!itsa.attsIncluded()) && (AssociationView.getConstrainedLowerBound(ref, fullClass) == 1)
&& (!nextFullClass.hasSomeFixedValue()) && (!ref.getName().equals("contentId")))
warnMissingAssociation(ref, fullClass);
/* put contentId nodes in the full mapping set, with no mappings or child nodes,
* so the wrapper transform will know when to add them */
if (ref.getName().equals("contentId"))
{
ElementDef nextFullNode = MapperFactory.eINSTANCE.createElementDef();
String fullNodeName = putInCorrectNamespace(ref.getName());
nextFullNode.setName(fullNodeName);
// multiplicities do not matter, as only the existence of the node is used
fullNode.getChildElements().add(nextFullNode);
}
/* carry on down the full class model only if the association has included attributes below it */
if (itsa.attsIncluded())
{
// true if this node requires its child nodes to be ordered
boolean orderChildNodes = itsa.childrenAreOrdered();
// warn the user if a <text> association has been renamed or collapsed
if (ref.getName().equals("text")) warnIfTextElementRenamed(fullClass, itsa);
// make a child node in the full mapping set, whether or not the association is collapsed
ElementDef nextFullNode = MapperFactory.eINSTANCE.createElementDef();
String fullNodeName = putInCorrectNamespace(ref.getName());
nextFullNode.setName(fullNodeName);
// initially take multiplicities from the association
setMultiplicities(nextFullNode,ref,fullClass);
// but if the association is collapsed, set max multiplicity in the full mapping set to 1
if (itsa.isCollapsed()) nextFullNode.setMaxMultiplicity(MaxMult.ONE);
// add a position attribute if the parent node required it
AttributeDef fullPosAttDef = null;
if (ordered) fullPosAttDef = addPositionAttributeDef(nextFullNode);
// mark the full mapping node with the CDA tag name (to be used when writing a schema)
String CDATagName = ModelUtil.getMIFAnnotation(ref, "CDA_Name");
if (CDATagName != null) nextFullNode.setDescription(CDA_NAME + CDATagName);
nextFullNode.setExpanded(true);
fullNode.getChildElements().add(nextFullNode);
// will be set and passed down the recursion
ElementDef nextSimpleNode = null;
LabelledEClass nextSimpleClass = null;
boolean nextZeroLowerBound = true;
// if the association is collapsed, do not change the simple node or the simple class
if (itsa.isCollapsed())
{
nextSimpleNode = simpleNode;
nextSimpleClass = simpleClass;
/* if any association in a chain of collapsed associations has lower bound 0,
* then the association in the simple class model has lower bound 0 */
nextZeroLowerBound = (zeroLowerBound || (AssociationView.getConstrainedLowerBound(ref, fullClass) == 0));
// recursive step
if (nextFullClass != null)
fillMappingNodes(nextFullClass,nextSimpleClass,nextFullNode,nextSimpleNode,nextZeroLowerBound,orderChildNodes);
else System.out.println("Null class at full mapping set path " + nextFullNode.getPath());
}
/* if the association is not collapsed and is manually retained for included attributes below it,
* make a new node in the simple mappings, and find or create a new class in the simple model */
else if ((!itsa.isCollapsed()) && (itsa.attsIncluded()))
{
//** ASSUMPTION: grouping nodes are not mixed with conditional node names in the same annotation
// detect renamed nodes and grouping nodes
String suggestedSimpleNodeName = ref.getName();
Vector<String> groupingNodeNames = new Vector<String>();
StringTokenizer st = new StringTokenizer(itsa.businessName(),"/"); // may have 0, 1, or many tokens
while (st.hasMoreTokens())
{
String nodeName = st.nextToken();
groupingNodeNames.add(nodeName);
suggestedSimpleNodeName = nodeName; // set to the last, if there are any; may be changed below
}
//** (not implemented) parse suggestedSimpleNodeName into N conditional node names, and iterate over them.
//** (not implemented) For each name, check that the condition value matches the condition value of simpleClass.
//** (not implemented) Must always rename associations after a conditional branch, if simpleClass has a condition value
/* When there are no grouping nodes, hang the new class on the parent simple class
* when there are grouping nodes, check if they hang in sequence from the current parent simple class */
LabelledEClass groupingClass = simpleClass;
ElementDef groupingNode = simpleNode;
/* when the annotation has a business name with one or more '/' such as 'group/node' ,
* introducing grouping nodes. Don't treat the last element, which is not a grouping node. */
for (int i = 0; i < groupingNodeNames.size()-1; i++)
{
String nextGroupingNodeName = groupingNodeNames.get(i);
/* check if the grouping class is already a child class of the parent class in the simple class model;
* if not, make it */
LabelledEClass nextGroupingClass = groupingClass.getNamedAssocChild(nextGroupingNodeName);
nextGroupingNode = groupingNode.getNamedChildElement(nextGroupingNodeName);
if (nextGroupingClass == null)
nextGroupingClass = makeNextGroupingClass(nextGroupingNodeName, sourceClassName,
groupingClass, groupingNode, fullNode, simpleClass);
// ready for end of iteration, or next round of iteration
groupingClass = nextGroupingClass;
groupingNode = nextGroupingNode;
} // end of grouping node code
/* things to do whether or not there is a grouping class */
// make or find the class in the simple class model
String nextSimpleClassName = makeClassName(suggestedSimpleNodeName);
nextSimpleClass = findOrMakeClass(getSimplePackageName(nextFullClass.eClass()),nextSimpleClassName,suggestedSimpleNodeName,groupingClass);
// give the class an ordering attribute if it needs one
EAttribute posAtt = null;
if (ordered) posAtt = addPositionEAttribute(nextSimpleClass);
//** (not implemented) if the simple node name has a path with a value condition, annotate these on the simple class
// make the containment association in the simple class model - possibly with a de-duped name
EReference newRef = makeSimpleModelERef(groupingClass, nextSimpleClass,
suggestedSimpleNodeName, sourceClassName);
// when you add a LabelledEClass child, it has an association name - but is it right?
if (groupingClass.getNamedAssocChild(newRef.getName()) == null) groupingClass.getChildren().add(nextSimpleClass);
/* make and name the new simple node and add it beneath its expected parent, if it is not there already.
* If it is there already, there should be a check of the multiplicities, but I have not yet implemented it.*/
nextSimpleNode = groupingNode.getNamedChildElement(newRef.getName());
if (nextSimpleNode == null)
{
nextSimpleNode = MapperFactory.eINSTANCE.createElementDef();
nextSimpleNode.setExpanded(true);
nextSimpleNode.setName(newRef.getName());
groupingNode.getChildElements().add(nextSimpleNode);
String desc = "From association '" + ref.getName() + "' of class '" + sourceClassName + "'";
nextSimpleNode.setDescription(desc);
}
// if the parent requires ordering of its children, add the simple AttributeDef and the mappings
if (ordered)
{
AttributeDef simplePosAttDef = addPositionAttributeDef(nextSimpleNode);
addPropertyMapping(simplePosAttDef,nextSimpleClass,posAtt);
addPropertyMapping(fullPosAttDef,nextSimpleClass,posAtt);
}
// get the multiplicities right in the simple class model and the simple mapping set
setCollapsedMultiplicities(fullClass,nextSimpleNode, ref,newRef,zeroLowerBound);
nextZeroLowerBound = false;
// add mappings, if there is not one already in the simple mapping set
if (!mappingExistsAlready(nextSimpleNode,nextSimpleClass))
{
makeObjectAndAssocMapping(nextSimpleNode,nextSimpleClass,newRef,groupingClass);
//** (not implemented) if nextSimpleClass has a value condition annotation, make the full mappings conditional
ObjMapping nextFullObjectMapping = makeObjectAndAssocMapping(nextFullNode,nextSimpleClass,newRef,groupingClass);
addFixedValueConditions(nextFullObjectMapping, nextFullClass,ref);
}
// recursive step
fillMappingNodes(nextFullClass,nextSimpleClass,nextFullNode,nextSimpleNode,nextZeroLowerBound,orderChildNodes);
//** (not implemented) end of new iteration over value conditions
} // end of ((!itsa.isCollapsed()) && (itsa.attsIncluded())) section
} // end of ((itsa.attsIncluded())||(autoRetain)) section
}
/**
*
* @param el
* @param theClass
* @return true if the ElementDef already has a mapping to the class - not sensitive to subsets
*/
private boolean mappingExistsAlready(ElementDef el, LabelledEClass theClass)
{
boolean found = false;
NodeMappingSet nms = el.getNodeMappingSet();
if (nms != null) for (Iterator<ObjMapping> it = nms.getObjectMappings().iterator();it.hasNext();)
{
ObjMapping om = it.next();
if (om.getClassSet().className().equals(ModelUtil.getQualifiedClassName(theClass.eClass()))) found = true;
}
return found;
}
/**
*
* @param att
* @param fullClass
* @param simpleClass
* @param fullNode
* @param simpleNode
* @param fullObjMapping
* @param zeroLowerBound
* @throws MapperException
*/
private void fillAttributeNode(EAttribute att,LabelledEClass fullClass,LabelledEClass simpleClass,
ElementDef fullNode,ElementDef simpleNode, boolean zeroLowerBound)
throws MapperException
{
ITSAttribute itsAtt = FeatureView.getITSAttribute(att, fullClass.getPath());
String fixedValue = fullClass.getAnnotatedFixedValue(att.getName());
String sourceClassName = fullClass.eClass().getName();
// warn where an attribute required in the full message cannot be supplied
if ((!itsAtt.isIncluded()) && (fixedValue == null)
&& (AttributeView.getConstrainedLowerBound(att, fullClass) == 1) && (!att.getName().equals("xsi:type")))
warnRequiredAttributeMissing(att, fullClass);
if (itsAtt.isIncluded())
{
/* warn the user if an attribute with a fixed value has been included in the simple model;
* it will not be included, as this would lead to gaps in translations */
if (fixedValue != null) warnIfFixedAttIncluded(att, fullClass, fixedValue);
else if (fixedValue == null)
{
String suggestedAttName = att.getName();
if (!(itsAtt.businessName().equals(""))) suggestedAttName = itsAtt.businessName();
//** (not implemented) parse suggestedAttName to get N different conditional names, and iterate over them
//** (not implemented) for each, check that the condition value matches the condition value of simpleClass
//** (not implemented) must always rename attributes after a conditional branch, if simpleClass has a condition value
// add the property to the simple class model, if it has not been added before; this may change the suggested name
EAttribute theAtt = addAttributeToSimpleClassModel(fullClass, simpleClass, att,suggestedAttName, sourceClassName,zeroLowerBound);
AttributeDef simpleAtt = null;
AttributeDef fullAtt = null;
// add an Attribute node to the simple mapping set, with possibly changed name, and map it
// add an Attribute node to the full mapping set, with the unchanged name, if it does not exist already
//** (not implemented) if the attribute has a condition value, add a mapping condition
if (!(att.getName().equals("textContent")))
{
simpleAtt = addSimpleAttribute(att, fullClass, simpleClass, simpleNode, sourceClassName, theAtt,zeroLowerBound);
fullAtt = addFullAttribute(att, fullClass, simpleClass, fullNode, simpleAtt, theAtt);
}
// for textContent nodes, add a property mapping on the ElementDef in each mapping set
else
{
addPropertyMapping(simpleNode,simpleClass, theAtt);
addPropertyMapping(fullNode,simpleClass, theAtt);
matchedNodes.put(fullNode, simpleNode); // note the correspondence between the two mapping sets
}
// add a row to the csv file of path-to-path mappings
addPathToPathMapping(att, fullNode, simpleNode, simpleAtt,fullAtt);
//** (not implemented) end of new iteration over conditional attribute names
} // end of (fixedValue == null) section
} // end of (itsAtt.isIncluded()) section
}
private void warnRequiredAttributeMissing(EAttribute att,
LabelledEClass fullClass) {
{
String warning = "Attribute " + att.getName() + " at path " + fullClass.getPath() +
" is required in the full message, but has not been included in the simplified message, and has no fixed value.";
warnAndAddWarningRow(fullClass.getPath(),"@" + att.getName(),"Attribute is required but not included or fixed",warning);
}
}
private AttributeDef addFullAttribute(EAttribute att,
LabelledEClass fullClass,LabelledEClass simpleClass, ElementDef fullNode,
AttributeDef simpleAtt, EAttribute theAtt) {
AttributeDef fullAtt;
{
fullAtt = fullNode.getNamedAttribute(att.getName());
if (fullAtt== null)
{
fullAtt = MapperFactory.eINSTANCE.createAttributeDef();
fullAtt.setName(att.getName());
setAttributeMultiplicity(fullAtt,AttributeView.getConstrainedLowerBound(att,fullClass));
fullNode.getAttributeDefs().add(fullAtt);
}
addPropertyMapping(fullAtt,simpleClass, theAtt);
matchedNodes.put(fullAtt, simpleAtt); // note the correspondence between the two mapping sets
}
return fullAtt;
}
private AttributeDef addSimpleAttribute(EAttribute att,
LabelledEClass fullClass,LabelledEClass simpleClass, ElementDef simpleNode,
String sourceClassName, EAttribute theAtt, boolean zeroLowerBound) {
AttributeDef simpleAtt;
{
simpleAtt = MapperFactory.eINSTANCE.createAttributeDef();
simpleAtt.setName(theAtt.getName());
String desc = "From attribute '" + att.getName() + "' of class '" + sourceClassName + "'";
simpleAtt.setDescription(desc);
setAttributeMultiplicity(simpleAtt,AttributeView.getConstrainedLowerBound(att,fullClass));
if (zeroLowerBound) setAttributeMultiplicity(simpleAtt,0);
simpleNode.getAttributeDefs().add(simpleAtt);
addPropertyMapping(simpleAtt,simpleClass, theAtt);
}
return simpleAtt;
}
private void warnIfFixedAttIncluded(EAttribute att,
LabelledEClass fullClass, String fixedValue) {
{
String warning = "Attribute '" + att.getName() + "' at path " + fullClass.getPath() +
" has fixed value '" + fixedValue + "', so cannot be included in the simple message.";
warnAndAddWarningRow(fullClass.getPath(),"@" + att.getName(),"Attribute has a fixed value so cannot be included in the simple message.",warning);
}
}
private void warnMissingAssociation(EReference ref, LabelledEClass fullClass) {
{
String warning = "Association " + ref.getName() + " at path " + fullClass.getPath()
+ " is required in the full message, but no attributes below this association have been included in the simple message. ";
warnAndAddWarningRow(fullClass.getPath(),ref.getName(),"Association is required but nothing below it is included",warning);
}
}
private void addPathToPathMapping(EAttribute att, ElementDef fullNode,
ElementDef simpleNode, AttributeDef simpleAtt, AttributeDef fullAtt) {
{
String fullPath = fullNode.getPath();
String simplePath = simpleNode.getPath();
if (!(att.getName().equals("textContent")))
{
fullPath = fullPath + "/@" + fullAtt.getName();
//** (not implemented) add any value condition in [] to fullPath
simplePath = simplePath + "/@" + simpleAtt.getName();
}
String[] csvRow = new String[5];
csvRow[0] = simplePath;
csvRow[1] = fullPath;
csvRow[2] = "";
csvRow[3] = "";
csvRow[4] = "";
attributeMappings.add(csvRow);
}
}
/**
*
* @param fullClass
* @param itsa
*/
private void warnIfTextElementRenamed(LabelledEClass fullClass,
ITSAssociation itsa)
{
{
String preface = "The association 'text' at path " + fullClass.getPath();
String rename = itsa.businessName();
if ((rename != null) && (!rename.equals("")))
{
String warning = preface + " has been renamed; its text subtree will not be retained.";
warnAndAddWarningRow(fullClass.getPath(),"text","Text node renamed; full text subtree will not be retained",warning);
}
if (itsa.isCollapsed())
{
String warning = preface + " has been flattened; its text subtree will not be retained.";
warnAndAddWarningRow(fullClass.getPath(),"text","Text node flattened; full text subtree will not be retained",warning);
}
}
}
/**
* make a grouping class, its association to its parent class, the grouping node in
* the simple message structure, and the mappings in simple and full mapping sets
*
* return the grouping class.
* Setting the grouping node in the simple structure
* as side-effects of method makeNextGroupingClass does not work.
* So I have had to make nextGroupingNode an instance variable of the class.
*
* @param nextGroupingNodeName
* @param sourceClassName
* @param groupingClass
* @param groupingNode
* @param fullNode
* @param simpleClass
* @throws MapperException
* @return the next grouping class
*/
private LabelledEClass makeNextGroupingClass(String nextGroupingNodeName, String sourceClassName,
LabelledEClass groupingClass, ElementDef groupingNode,
ElementDef fullNode, LabelledEClass simpleClass)
throws MapperException
{
String nextGroupingClassName = makeClassName(nextGroupingNodeName);
String packageName = groupingClass.eClass().getEPackage().getName();
LabelledEClass nextGroupingClass = findOrMakeClass(packageName,nextGroupingClassName,nextGroupingNodeName,groupingClass);
groupingClass.getChildren().add(nextGroupingClass);
// make the containment association to the grouping class in the simple class model - failing if the name changes
EReference nextGroupingRef = makeSimpleModelERef(groupingClass, nextGroupingClass,
nextGroupingNodeName, sourceClassName);
if (!nextGroupingRef.getName().equals(nextGroupingNodeName))
throw new MapperException("Cannot change grouping node name from '" + nextGroupingNodeName + "' to '" + nextGroupingRef.getName() + "'");
// make and name the grouping node, and add it beneath its expected parent
nextGroupingNode = MapperFactory.eINSTANCE.createElementDef();
nextGroupingNode.setExpanded(true);
nextGroupingNode.setName(nextGroupingNodeName);
nextGroupingNode.setDescription("Grouping association");
groupingNode.getChildElements().add(nextGroupingNode);
// set multiplicities appropriate for a grouping node
nextGroupingRef.setLowerBound(1);
nextGroupingRef.setUpperBound(1);
nextGroupingNode.setMinMultiplicity(MinMult.ONE);
nextGroupingNode.setMaxMultiplicity(MaxMult.ONE);
// make mappings to the next grouping class in simple and full mapping sets
// simple mappings to the grouping class on the grouping node
makeObjectAndAssocMapping(nextGroupingNode,nextGroupingClass,nextGroupingRef,groupingClass);
// full mappings to the next grouping class on the node representing the parent class
ElementDef parentMappingNode = classMappingNode(fullNode,simpleClass);
makeObjectAndAssocMapping(parentMappingNode,nextGroupingClass,nextGroupingRef,groupingClass);
// this object mapping has no fixed value conditions
return nextGroupingClass;
}
/**
* for full mapping sets for NHS CDAs, some elements must be put in the NHS localisation namespace
* @param fullNodeName
* @return
*/
private String putInCorrectNamespace(String fullNodeName)
{
String result = fullNodeName;
if (isNHSMIF())
{
if (GenUtil.inArray(fullNodeName, NHS_CDA_Wrapper.IN_NHS_NAMESPACE))
result = NHS_CDA_Wrapper.NHSPREFIX + ":" + fullNodeName;
}
return result;
}
/**
*
* @param fullNode
* @param simpleClass
* @return fullNode or the node above it which has a mapping to class simpleClass
*/
private ElementDef classMappingNode(ElementDef fullNode,LabelledEClass simpleClass) throws MapperException
{
String className = simpleClass.eClass().getName();
ElementDef mappingNode = fullNode;
boolean found = false;
while ((!found) && (mappingNode != null))
{
if (mappingNode.getNodeMappingSet() != null)
{
EList<ObjMapping> mappings = mappingNode.getNodeMappingSet().getObjectMappings();
for (Iterator<ObjMapping> it = mappings.iterator();it.hasNext();)
{
ObjMapping mapping = it.next();
if (mapping.getMappedClass().equals(className)) found = true;
}
}
if (!found)
{
EObject parent = mappingNode.eContainer();
if (parent instanceof ElementDef) mappingNode = (ElementDef)parent;
else mappingNode = null;
}
}
if (!found) throw new MapperException("Cannot find mapping for class " + className + " in path " + fullNode.getPath());
return mappingNode;
}
/**
* make a new EAttribute in the simple class model, if necessary.
* If the EAttribute with the suggested name already exists, reuse it.
* @param simpleClass
* @param att
* @param suggestedAttName
* @param sourceClassName
* @return
*/
private EAttribute addAttributeToSimpleClassModel(LabelledEClass fullClass,LabelledEClass simpleClass, EAttribute att,
String suggestedAttName,String sourceClassName, boolean zeroLowerBound)
{
String aName = suggestedAttName;
// find if the simple class already has an EAttribute of this name
EStructuralFeature previous = simpleClass.eClass().getEStructuralFeature(aName);
if ((previous != null) && (previous instanceof EAttribute))
{
// extend the 'sourceClass' annotation if necessary; but I don't think it is used
String previousSources = ModelUtil.getMIFAnnotation(previous, "sourceClass");
StringTokenizer st = new StringTokenizer(previousSources,";");
boolean foundSource = false;
while (st.hasMoreTokens()) if (st.nextToken().equals(sourceClassName)) foundSource = true;
if (!foundSource) ModelUtil.addMIFAnnotation(previous,"sourceClass",previousSources + ";" + sourceClassName);
return (EAttribute)previous;
}
EAttribute theAtt = EcoreFactory.eINSTANCE.createEAttribute();
theAtt.setName(aName);
theAtt.setEType(EcorePackage.eINSTANCE.getEString());
theAtt.setLowerBound(AttributeView.getConstrainedLowerBound(att, fullClass));
if (zeroLowerBound) theAtt.setLowerBound(0);
ModelUtil.addMIFAnnotation(theAtt, "sourceClass", sourceClassName);
simpleClass.eClass().getEStructuralFeatures().add(theAtt);
return theAtt;
}
/**
* make a new EReference in the simple class model, if necessary.
* If the ERef with the suggested name already exists, reuse it.
* @param simpleClass
* @param nextSimpleClass
* @param suggestedSimpleNodeName
* @param fullClassName
* @return
*/
private EReference makeSimpleModelERef(LabelledEClass simpleClass, LabelledEClass nextSimpleClass,
String suggestedSimpleNodeName, String sourceClassName)
{
String refName = suggestedSimpleNodeName;
// find if the simple class already has an EReference of this name
EStructuralFeature previous = simpleClass.eClass().getEStructuralFeature(refName);
if ((previous != null) && (previous instanceof EReference))
{
// extend the 'sourceClass' annotation if necessary; but I don't think it is used
String previousSources = ModelUtil.getMIFAnnotation(previous, "sourceClass");
StringTokenizer st = new StringTokenizer(previousSources,";");
boolean foundSource = false;
while (st.hasMoreTokens()) if (st.nextToken().equals(sourceClassName)) foundSource = true;
if (!foundSource) ModelUtil.addMIFAnnotation(previous,"sourceClass",previousSources + ";" + sourceClassName);
return (EReference)previous;
}
// make the containment association in the simple class model
EReference newRef = EcoreFactory.eINSTANCE.createEReference();
newRef.setName(refName);
newRef.setContainment(true);
newRef.setEType(nextSimpleClass.eClass());
// annotate it with the full class it came from
ModelUtil.addMIFAnnotation(newRef, "sourceClass", sourceClassName);
simpleClass.eClass().getEStructuralFeatures().add(newRef);
return newRef;
}
/**
* add all necessary fixed value condition to an object mapping in the full mapping set
* @param fullObjMapping
* @param fullClass
* @throws MapperException
*/
private void addFixedValueConditions(ObjMapping fullObjMapping, LabelledEClass fullClass, EReference ref) throws MapperException
{
String path = "";
@SuppressWarnings("unused")
String refName = "none";
if (ref !=null) refName = ref.getName();
@SuppressWarnings("unused")
String className = fullObjMapping.getMappedClass();
// System.out.println("Fixed values on object mapping " + className + " with full class " + fullClass.eClass().getName() + " and ref " + refName);
addDeepFixedValueConditions(path, fullObjMapping, fullClass,ref);
}
/**
* recursion down 1..1 associations to add all necessary fixed value conditions to an object mapping in the full mapping set
* @param path path through associations so far - e.g "" or "className/"
* @param fullObjMapping
* @param fullClass
* @throws MapperException
*/
private void addDeepFixedValueConditions(String path, ObjMapping fullObjMapping, LabelledEClass fullClass, EReference ref) throws MapperException
{
StringTokenizer st = new StringTokenizer(path,"/");
int maxDepth = 8;
if (st.countTokens() > maxDepth) throw new MapperException("Path '" + path + "' goes too deep when looking for fixed attribute values");
/* deal with NHS fixed values, annotated on associations to data type classes.
* These include extensions of templateIds, which are II */
if (ref != null) // ref is null for the top call from ClinicalDocument
{
String fixedVal = ModelUtil.getEAnnotationDetail(ref,"fixed att value");
if (fixedVal != null)
{
String attName = getNHSFixedAttributeName(fullClass.eClass().getName());
basicAddFixedValue(path, fullObjMapping, attName, fixedVal);
}
/* we do not have the value of extension for an npfitlc:contentId attribute attached to it as an annotation.
* find it out from a grandchild templateId extension value annotation. */
else if (ref.getName().equals("contentId"))
{
String extensionValue = getGrandchildTemplateIdExtension(ref);
if (extensionValue != null)
{
basicAddFixedValue(path, fullObjMapping, "root", NHSContentIdRoot);
basicAddFixedValue(path, fullObjMapping, "extension", extensionValue);
}
}
}
// find attributes of this class with fixed values
for (Iterator<EAttribute> it = fullClass.eClass().getEAllAttributes().iterator();it.hasNext();)
{
EAttribute att = it.next();
String attName = att.getName();
// fixed values of some attributes may be suppressed
if ((suppressFixedTextMappings) && (GenUtil.inArray(attName, suppressableAttribute))) {}
else
{
// get any kind of fixed value of the attribute - from the MIF file, or set by the user
String fixedVal = fullClass.getAnnotatedFixedValue(attName);
if (fixedVal != null) basicAddFixedValue(path, fullObjMapping, attName, fixedVal);
}
}
// find the names of associations to grouping classes
Hashtable<String,String> groupers = groupingAssociations(fullClass);
/* recursion down through all 1..1 associations,
* leaving out those which have attributes beneath them and are not flattened, because fixed values
* below those nodes will go to a lower object mapping */
for (Iterator<EReference> it = fullClass.eClass().getEAllReferences().iterator();it.hasNext();)
{
EReference nextRef = it.next();
if ((AssociationView.getConstrainedLowerBound(nextRef, fullClass) == 1)
&& (AssociationView.getConstrainedUpperBound(nextRef, fullClass) == 1))
{
/* do nothing if there is a lower object mapping to hang the fixed values on
* - that is , if the association has retained attributes beneath it, it is not collapsed,
* and its name is not the name of an association to a grouping class */
ITSAssociation itsa = FeatureView.getITSAssociation(nextRef, fullClass.getPath());
if ((itsa.attsIncluded())
&& (!itsa.isCollapsed())
&& (groupers.get(itsa.businessName()) == null)) {}
else
{
LabelledEClass target = fullClass.getNamedAssocChild(nextRef.getName());
String newPath = path + putInCorrectNamespace(nextRef.getName()) + "/";
if (target != null) addDeepFixedValueConditions(newPath,fullObjMapping,target,nextRef);
else System.out.println("Null class at path " + newPath);
}
}
}
}
/**
*
* @param ref a 'contentId' reference from some class in the source class model
* @return if the class has a child class with a templateId child, return the fixed att value annotation of that ref,
* which is the template id. Otherwise return null.
*/
private String getGrandchildTemplateIdExtension(EReference ref)
{
EClass ownerClass = (EClass)ref.eContainer();
String idValue = null;
// look for a child class with a templateId association
for (Iterator<EReference> it = ownerClass.getEAllReferences().iterator();it.hasNext();)
{
EClass childClass = (EClass)it.next().getEType();
EStructuralFeature feat = childClass.getEStructuralFeature("templateId");
if (feat != null)
{
String fixedVal = ModelUtil.getEAnnotationDetail(feat,"fixed att value");
if (fixedVal != null) idValue = fixedVal;
}
}
return idValue;
}
/**
* collect the names of all associations to grouping classes immediately beneath this class
* @param fullClass
* @return
*/
private Hashtable<String,String> groupingAssociations(LabelledEClass fullClass)
{
Hashtable<String,String> assocs = new Hashtable<String,String>();
for (Iterator<EReference> it = fullClass.eClass().getEAllReferences().iterator();it.hasNext();)
{
EReference nextRef = it.next();
ITSAssociation itsa = FeatureView.getITSAssociation(nextRef, fullClass.getPath());
if ((itsa != null) && (itsa.businessName() != null))
{
StringTokenizer st = new StringTokenizer(itsa.businessName(),"/");
// for some associations, the grouping ref name is followed by '/' and sme other ref name
if (st.countTokens() > 1)
{
String groupingRef = st.nextToken();
assocs.put(groupingRef, "1");
}
}
}
return assocs;
}
private void basicAddFixedValue(String path, ObjMapping fullObjMapping,
String attName, String fixedVal) {
{
String attPath = path + "@" + attName;
// make the Value condition and add it, without making duplicates (unnecessary?)
ValueCondition vc = MapperFactory.eINSTANCE.createValueCondition();
vc.setLeftPath(attPath);
vc.setRightValue(fixedVal);
if (getValueCondition(fullObjMapping,attPath) == null)
fullObjMapping.getMappingConditions().add(vc);
}
}
private String getNHSFixedAttributeName(String dataTypeName) throws MapperException
{
String attName="";
for (int i = 0; i < NHSFixedAttributes.length; i++)
{
String[] fixedAtt = NHSFixedAttributes[i];
if (dataTypeName.equals(fixedAtt[0])) attName = fixedAtt[1];
}
if (attName.equals("")) throw new MapperException("No defined attribute for fixed value of class '" + dataTypeName + "'");
return attName;
}
private ValueCondition getValueCondition(ObjMapping fullObjMapping,String path)
{
ValueCondition vc = null;
for (Iterator<MappingCondition> ic = fullObjMapping.getMappingConditions().iterator();ic.hasNext();)
{
MappingCondition mc = ic.next();
if (mc instanceof ValueCondition)
{
ValueCondition v = (ValueCondition)mc;
if (v.getLeftPath().equals(path)) vc = v;
}
}
return vc;
}
/**
* set the multiplicities of a child ElementDef
* and an EReference in the simplified model according to the
* multiplicities of the EReference in the full model,
* with min multiplicity pushed down to zero if some higher association was
* collapsed and had min multiplicity zero.
* @param next
* @param ref
*/
private void setCollapsedMultiplicities(LabelledEClass fullClass, ElementDef next,EReference ref, EReference newRef, boolean zeroLowerBound)
{
next.setMinMultiplicity(MinMult.ONE);
newRef.setLowerBound(1);
if ((AssociationView.getConstrainedLowerBound(ref, fullClass) == 0)|| zeroLowerBound)
{
next.setMinMultiplicity(MinMult.ZERO);
newRef.setLowerBound(0);
}
next.setMaxMultiplicity(MaxMult.ONE);
if (AssociationView.getConstrainedUpperBound(ref, fullClass) == -1) next.setMaxMultiplicity(MaxMult.UNBOUNDED);
newRef.setUpperBound(ref.getUpperBound());
}
private void setAttributeMultiplicity(AttributeDef attDef, int min)
{
if (min == 0) attDef.setMinMultiplicity(MinMult.ZERO);
if (min == 1) attDef.setMinMultiplicity(MinMult.ONE);
}
/**
* make a class name from an association name, by converting its initial letter
* to upper case
* (if the initial letter is already upper case, write a gentle warning to the console.
* having class names and association names coincide is not a disaster)
* @param assocName
* @return
* @throws MapperException
*/
private String makeClassName(String assocName) throws MapperException
{
String first = assocName.substring(0, 1);
String lowerFirst = first.toLowerCase();
if (!(first.equals(lowerFirst))) System.out.println("Association name '" + assocName + "' should ideally start in lower case.");
String upperFirst = first.toUpperCase();
return (upperFirst + assocName.substring(1));
}
/**
*
* @param className a class in the simple class model
* @param parent the parent LabelledEClass
* @return the subset to be used in mappings to this occurrence of the class.
* Different parent paths are guaranteed to give different subsets
*/
private String getSubset(String className, LabelledEClass parent)
{
String subset = "";
// only one subset "" is needed for the root class which has no parent
if (parent == null) return subset;
Hashtable<String,String> subsetsForThisClass = subsetsForPaths.get(className);
if (subsetsForThisClass == null)
{
subset = ""; // for clarity
subsetsForThisClass = new Hashtable<String,String>();
subsetsForThisClass.put(parent.getPath(), subset);
subsetsForPaths.put(className, subsetsForThisClass);
}
else if (subsetsForThisClass != null)
{
subset = subsetsForThisClass.get(parent.getPath());
if (subset == null)
{
subset = "s" + subsetsForThisClass.size(); // must be different from all existing ones
subsetsForThisClass.put(parent.getPath(), subset);
subsetsForPaths.put(className, subsetsForThisClass);
}
}
// trace("subset for class " + className + " at path " + parent.getPath() + " : '" + subset + "'");
return subset;
}
/**
*
* @param fullClass current class in the full CDA class model, corresponding to a node in the CDA
* @return an appropriate name for the package of the simple class model, which the node in the
* simple and full messages are going to be mapped to. This package name is 'CDA_DT' for data type classes,
* or 'CDA_' followed by a readable template name for any RIM class
*
*/
private String getSimplePackageName(EClass fullClass)
{
EPackage fullPackage = selfOrChild(fullClass).getEPackage();
if (fullPackage.getName().equals("datatypes")) return "datatypes";
String readablePackageName = ModelUtil.getMIFAnnotation(fullPackage, "name"); // e.g. 'PatientUniversal'
if (readablePackageName == null) readablePackageName = fullPackage.getName(); // e.g. 'POCD_RM010011GB02' ;should not happen
// package names cannot have spaces, and start in lower case
String simplePackageName = removeSpaces(readablePackageName);
// store the link between template name and template id
TemplatePackageIds.put(simplePackageName, fullPackage.getName());
return simplePackageName;
}
/**
*
* @param s
* @return s with spaces removed, and initial letter in lower case
*/
private String removeSpaces(String s)
{
String result = "";
// remove spaces
StringTokenizer st = new StringTokenizer(s," ");
while (st.hasMoreTokens()) result = result + st.nextToken();
// make the first letter lower case
result = result.substring(0, 1).toLowerCase() + result.substring(1);
return result;
}
/**
*
* @param theClass
* @return the class itself, or, for an ActRelationship or a Participation,
* the one child class which is a RIM class
*/
private EClass selfOrChild(EClass theClass)
{
EClass result = theClass;
String[] dropClasses = {"ActRelationship","Participation"};
String RIMClass = ModelUtil.getMIFAnnotation(theClass,"RIM Class");
// for ActRelationships and Participations, return the child class which is not a data type class
if ((RIMClass != null) && (GenUtil.inArray(RIMClass, dropClasses)))
{
for (Iterator<EReference> it = theClass.getEReferences().iterator();it.hasNext();)
{
EReference ref = it.next();
EClass target = (EClass)ref.getEType();
if (!target.getEPackage().getName().equals("datatype")) result = target;
}
}
return result;
}
// for generating namespace prefixes for packages in the simle class model
private int prefixIndex = 1;
/**
*
* @param packageName
* @return
*/
private EPackage findOrMakePackage(String packageName)
{
EPackage namedPackage = null;
for (Iterator<EPackage> it = topPackage.getESubpackages().iterator();it.hasNext();)
{
EPackage next = it.next();
if (next.getName().equals(packageName)) namedPackage = next;
}
if (namedPackage == null)
{
namedPackage = EcoreFactory.eINSTANCE.createEPackage();
namedPackage.setName(packageName);
namedPackage.setNsPrefix("cd" + prefixIndex);
namedPackage.setNsURI("com.openMap1." + packageName);
String templateId = TemplatePackageIds.get(packageName);
if (templateId != null) addDocumentation(namedPackage, "Package for CDA Template " + templateId);
prefixIndex++;
topPackage.getESubpackages().add(namedPackage);
}
return namedPackage;
}
/**
* If an EClass does not exist, make it and wrap it up in a LabelledEClass, with subset ""
* If it does exist, wrap it up in a LabelledEClass with a new unique subset
* Link the labelledEClass to a parent if the parent is not null
* @param className
* @param assocName
* @param parent
* @return
* @throws MapperException
*/
private LabelledEClass findOrMakeClass(String packageName,String className,String assocName,LabelledEClass parent) throws MapperException
{
// special to ensure that class 'Text' is always in the datatypes package
if (className.equals("Text")) packageName = "datatypes";
EPackage thePackage = findOrMakePackage(packageName);
String qualifiedClassName = packageName + "." + className;
EClass aClass = null;
LabelledEClass result = null;
// (a) EClass is already a child of the same parent LabelledEClass; nothing new to make
if (parent != null) result = parent.getNamedAssocAndClassChild(assocName, className);
if (result != null) return result;
// (b) EClass has been made as a child of a different parent
aClass = simpleClasses.get(qualifiedClassName);
// (c) EClass does not exist yet; make it
if (aClass == null)
{
aClass = EcoreFactory.eINSTANCE.createEClass();
aClass.setName(className);
thePackage.getEClassifiers().add(aClass);
simpleClasses.put(qualifiedClassName,aClass);
}
if (assocName == null) result = new LabelledEClass(aClass);
else result = new LabelledEClass(aClass,assocName,parent);
return result;
}
/**
*
* @throws MapperException
*/
private void makeInitialModelAndMappings() throws MapperException
{
// make an empty simplified Ecore model
topPackage = EcoreFactory.eINSTANCE.createEPackage();
String topPackageName="ccda"; // non-UK CDAs are expected to be consolidated CDA
if (isNHSMIF()) topPackageName = "cda";
topPackage.setName(topPackageName);
// top package needs a URI and a prefix for EMF code generation
topPackage.setNsPrefix("cd");
topPackage.setNsURI("com.OpenMap1.cda1"); // can the uri be the same as the name?
// ensure this class model will be viewed as an RMIM in the class model view
ModelUtil.addMIFAnnotation(topPackage, "RMIM", "true");
// if a message name has been defined, add it as an annotation on the top package
if (messageName() != null) ModelUtil.addMIFAnnotation(topPackage, "messageName", messageName());
// this model will not be used by a CDA Wrapper class. Point to the model which will.
String sourceModelFileName = "";
String sourceModelURL = mappedStructure.getUMLModelURL();
StringTokenizer st = new StringTokenizer(sourceModelURL,"/");
while (st.hasMoreTokens()) sourceModelFileName = st.nextToken();
ModelUtil.addMIFAnnotation(topPackage, "CDAWrapperModel", sourceModelFileName);
// save the empty Ecore model
String sourceModelLocation = ecoreRoot.eResource().getURI().toString();
System.out.println("Source model location: " + sourceModelLocation);
String simpleModelLocation = addSuffixToFileName(sourceModelLocation,"simple");
ModelUtil.savePackage(simpleModelLocation, topPackage);
// make and save an empty mapping set for the simple message structure
String simpleMappingLocation = addSuffixToFileName(mappingSetURIString,"simple");
simpleMappings = ModelUtil.saveNewMappingSet(simpleMappingLocation);
simpleMappings.setUMLModelURL(simpleModelLocation);
simpleMappings.setClassModelRoot(topPackage);
// conditionally, the mapping set is in the V3 namespace with no prefix
if (CDAConverter.SIMPLE_MESSAGE_IN_V3_NAMESPACE)
addNamespace("",CDAConverter.V3NAMESPACEURI,simpleMappings);
// make and save an empty mapping set for the full message structure
String fullMappingLocation = addSuffixToFileName(mappingSetURIString,"full");
fullMappings = ModelUtil.saveNewMappingSet(fullMappingLocation);
fullMappings.setUMLModelURL(simpleModelLocation);
fullMappings.setClassModelRoot(topPackage);
/* the full mappings may have the same wrapper class as the open mapping set,
* but if that has no wrapper class, the wrapper class is taken from an annotations on
* the entry class of the full class model*/
fullMappingsWrapperClassName = mappedStructure.getMappingParameters().getWrapperClass();
if (fullMappingsWrapperClassName.equals(""))
{
EClass topClass = classModelView.topLabelledEClass().eClass();
fullMappingsWrapperClassName = ModelUtil.getMIFAnnotation(topClass, "wrapperClass");
}
/* For NHS RMIMs, there are two possible wrapper classes (CDA wired, and plain V3).
* The CDA wired wrapper class will be applied later if the user chooses to read a CDA mapping set. */
if (isNHSMIF())
{
fullMappingsWrapperClassName = NHSV3WrapperClass;
// add the NHS localisation namespace to the full mapping set
addNamespace(NHS_CDA_Wrapper.NHSPREFIX,NHS_CDA_Wrapper.NHSURI,fullMappings);
}
// if the user has added any namespaces to the open mapping set, transfer them to the full mapping set
for (Iterator<Namespace> it = mappedStructure.getMappingParameters().getNameSpaces().iterator();it.hasNext();)
{
Namespace nSpace = it.next();
addNamespace(nSpace.getPrefix(),nSpace.getURL(),fullMappings);
}
// make the full mappings use the V3 namespace with no prefix
addNamespace("",CDAConverter.V3NAMESPACEURI,fullMappings);
addNamespace("xsi",XMLUtil.SCHEMAINSTANCEURI,fullMappings);
/* attach the correct wrapper class to the full mappings;
* and a wrapper to the simple mappings to pass through html subtrees unchanged*/
if (fullMappingsWrapperClassName != null)
{
fullMappings.getMappingParameters().setWrapperClass(fullMappingsWrapperClassName);
simpleMappings.getMappingParameters().setWrapperClass(simpleMappingsWrapperClassName);
}
}
/**
* add a namespace declaration to a mapping set.
* It is OK to have the same namespace URI with two prefixes, but not to repeat any prefix.
* @param prefix
* @param uri
* @param mappings
*/
private void addNamespace(String prefix, String uri, MappedStructure mappings)
{
boolean hasPrefixAlready = false;
for (Iterator<Namespace> it = mappings.getMappingParameters().getNameSpaces().iterator();it.hasNext();)
if (it.next().getPrefix().equals(prefix)) hasPrefixAlready = true;
if (!hasPrefixAlready)
{
Namespace ns = MapperFactory.eINSTANCE.createNamespace();
ns.setPrefix(prefix);
ns.setURL(uri);
mappings.getMappingParameters().getNameSpaces().add(ns);
}
}
private String addSuffixToFileName(String fullLocation,String suffix) throws MapperException
{
String result = null;
String[] exts = {".mapper",".ecore"};
for (int i = 0; i < exts.length; i++)
{
if (fullLocation.endsWith(exts[i]))
{
int len = fullLocation.length() - exts[i].length();
result = fullLocation.substring(0,len) + "_" + suffix + exts[i];
}
}
if (result == null) throw new MapperException("Invalid file extension in location '" + fullLocation + "'");
return result;
}
private String changeExtension(String fullLocation, String newExtension) throws MapperException
{
StringTokenizer st = new StringTokenizer(fullLocation,".");
if (st.countTokens() != 2) throw new MapperException("Unexpected file path: " + fullLocation);
return (st.nextToken() + "." + newExtension);
}
/**
* constrain the mapped structure under topSimpleNode so that
* its tree structure is a subset of the tree structure in masterStructure,
* ignoring different namespaces and allowing different names on the top node
* @param topSimpleNode
* @param masterStructure
*/
private ElementDef constrainSimpleStructure(ElementDef topSimpleNode,MappedStructure masterStructure) throws MapperException
{
ElementDef topMasterNode = masterStructure.getRootElement();
StructureDefinition master = masterStructure.getStructureDefinition();
return constrainSimpleSubtree(topSimpleNode,topMasterNode,master);
}
/**
* recursive descent, removing nodes from the simple structure which do not match nodes in the master structure
* @param simpleNode
* @param masterNode
* @param master
* @return
* @throws MapperException
*/
private ElementDef constrainSimpleSubtree(ElementDef simpleNode,ElementDef masterNode,StructureDefinition master) throws MapperException
{
ElementDef masterEl = masterNode;
if (!masterNode.isExpanded()) masterEl = master.typeStructure(masterNode.getType());
if (masterEl == null) throw new MapperException("Cannot expand constraining mapped structure at node " + masterNode.getName());
// filter the list of simple child elements to be a subset of those in the master structure
EList<ElementDef> newChildElements = new BasicEList<ElementDef>();
for (Iterator<ElementDef> it = simpleNode.getChildElements().iterator(); it.hasNext();)
{
ElementDef simpleChild = it.next();
ElementDef masterChild = getMasterChild(masterEl,simpleChild.getName());
if (masterChild != null) newChildElements.add(constrainSimpleSubtree(simpleChild,masterChild, master));
else System.out.println("Removed simple message node at " + simpleChild.getPath());
}
simpleNode.eSet(MapperPackage.Literals.ELEMENT_DEF__CHILD_ELEMENTS, newChildElements);
return simpleNode;
}
/**
*
* @param masterEl
* @param name
* @return a child element of element masterEl, which has the name 'name', apart from namespace prefixes;
* or null if there is no such element
*/
private ElementDef getMasterChild(ElementDef masterEl, String name)
{
ElementDef result = null;
for (Iterator<ElementDef> it = masterEl.getChildElements().iterator(); it.hasNext();)
{
String localName = "";
ElementDef mc = it.next();
// remove any namespace prefix from the node name
StringTokenizer st = new StringTokenizer(mc.getName(),":");
while (st.hasMoreTokens()) localName = st.nextToken();
if (name.equals(localName)) result = mc;
}
return result;
}
//-------------------------------------------------------------------------------------
// Final changes to full mapping set for NHS CDAs - to annotate nodes
// with CDA node names (for use in the wrapper transform) and reorder nodes,
// and to mark attributes on the simple and full mapping sets with data types
//-------------------------------------------------------------------------------------
//-------------------------------------------------------------------------------------
// Marking attributes of simple and full mapping sets with data types
//-------------------------------------------------------------------------------------
/**
*
*/
private void markMappingSetsWithTypes() throws MapperException
{
String prompt = "Open specific mapping set for data types of attributes";
String annotationKey = "specificCDAWithDataTypes";
MappedStructure CDAStructure = getCDAWireMappingSet(prompt,annotationKey);
// user may not choose a CDA mapping set; in that case do nothing to the full mapping set
if (CDAStructure != null)
{
ElementDef CDARoot = CDAStructure.getRootElement();
ElementDef fullRoot = fullMappings.getRootElement();
findDataTypes(CDAStructure,CDARoot,fullRoot);
}
}
/**
*
* @param CDAStructure
* @param CDAElDef
* @param fullElDef
* @throws MapperException
*/
private void findDataTypes(MappedStructure CDAStructure,ElementDef CDAElDef, ElementDef fullElDef)
throws MapperException
{
ElementDef cdaEl = CDAElDef;
if (!CDAElDef.isExpanded()) cdaEl = CDAStructure.getStructureDefinition().typeStructure(CDAElDef.getType());
if (cdaEl == null) throw new MapperException("Cannot find structure for node "
+ CDAElDef.getName() + " of type " + CDAElDef.getType());
cdaEl.setName(CDAElDef.getName());
// find types of all the attributes, and mark them
for (Iterator<AttributeDef> it = fullElDef.getAttributeDefs().iterator();it.hasNext();)
{
AttributeDef fullAtt = it.next();
AttributeDef cdaAtt = cdaEl.getNamedAttribute(fullAtt.getName()) ;
if (cdaAtt == null)
{
trace("Cannot find attribute " + fullAtt.getName() + " at path " + fullElDef.getPath());
}
else
{
String type = cdaAtt.getType();
if ((type != null) && (!type.equals("")))
{
AttributeDef simpleAtt = (AttributeDef)matchedNodes.get(fullAtt);
fullAtt.setType(type);
if (simpleAtt != null) simpleAtt.setType(type);
}
}
}
// recursive descent of the full mapping set tree
for (Iterator<ElementDef> it = fullElDef.getChildElements().iterator(); it.hasNext();)
{
ElementDef fullChild = it.next();
String childName = fullChild.getName();
// if the child node does not have a CDA node name, find the right one for it
ElementDef cdaChild = cdaEl.getNamedChildElement(childName);
if (cdaChild == null)
{
// try the cda name of the full mapping set node
String description = fullChild.getDescription();
if ((description != null) && (description.startsWith(CDA_NAME)))
{
String cdaName = description.substring(CDA_NAME.length());
cdaChild = cdaEl.getNamedChildElement(cdaName);
}
if (cdaChild == null) trace("Cannot find child element " + childName + " at path " + fullElDef.getPath());
}
if (cdaChild != null) findDataTypes(CDAStructure, cdaChild,fullChild);
}
}
//-------------------------------------------------------------------------------------
// Adding CDA wire-form paths and data types to the mapping csv file
//-------------------------------------------------------------------------------------
/**
* tidy up rows of a mappings csv file by removing the top element name from the paths
* @param mappingRows
* @return
* @throws MapperException
*/
private Vector<String[]> finalMappingRows(Vector<String[]> mappingRows) throws MapperException
{
int[] removeChars = {0,0,0};
// find the length of the top element name + 1 for '/', if there are any rows
if (mappingRows.size() > 1)
{
for (int col = 0; col < 3; col++)
{
StringTokenizer st = new StringTokenizer(mappingRows.get(1)[col],"/");
if (st.hasMoreTokens()) removeChars[col] = st.nextToken().length() + 1;
}
}
Vector<String[]> finalRows = new Vector<String[]>();
// do nothing to the header row
finalRows.add(mappingRows.get(0));
for (int i = 1; i < mappingRows.size(); i++)
{
String[] csvRow = mappingRows.get(i);
String fullPath = csvRow[1];
// remove the initial "/ClinicalDocument" or other top element name from the first 3 columns
csvRow[0] = csvRow[0].substring(removeChars[0]);
csvRow[1] = csvRow[1].substring(removeChars[1]);
csvRow[2] = cdaWireXPath(fullPath).substring(removeChars[2]);
csvRow[4] = dataTypeName(fullPath);
finalRows.add(csvRow);
}
return finalRows;
}
/**
* return the cda wire form path corresponding to a path in the full mapping set
*/
private String cdaWireXPath(String fullMappingSetXPath) throws MapperException
{
String cdaPath = "";
ElementDef fullEl = fullMappings.getRootElement();
StringTokenizer st = new StringTokenizer(fullMappingSetXPath,"/");
boolean top = true;
while (st.hasMoreTokens())
{
String step = st.nextToken();
String cdaStep = step;
if (!step.startsWith("@"))
{
if (!top) fullEl = fullEl.getNamedChildElement(step);
if (fullEl == null) throw new MapperException("Cannot find child element " + step + " in full mapping set");
String desc = fullEl.getDescription();
if (desc.startsWith(CDA_NAME)) cdaStep = desc.substring(CDA_NAME.length());
}
cdaPath = cdaPath + "/" + cdaStep;
top = false;
}
return cdaPath;
}
private String dataTypeName(String fullMappingSetXPath) throws MapperException
{
String typeName = "";
ElementDef fullEl = fullMappings.getRootElement();
StringTokenizer st = new StringTokenizer(fullMappingSetXPath,"/");
boolean top = true;
while (st.hasMoreTokens())
{
String step = st.nextToken();
if (step.startsWith("@"))
{
AttributeDef attDef = fullEl.getNamedAttribute(step.substring(1));
if (attDef == null) throw new MapperException("Cannot find attribute " + step.substring(1) + " in full mapping set");
typeName = attDef.getType();
}
else
{
if (!top) fullEl = fullEl.getNamedChildElement(step);
if (fullEl == null) throw new MapperException("Cannot find child element " + step + " in full mapping set");
}
top = false;
}
return typeName;
}
//-------------------------------------------------------------------------------------
// Ordering the full mapping set, and marking it with CDA tag names
//-------------------------------------------------------------------------------------
/**
* If the ecore model has a note of a cda wire form mapping set to use
* to order the full mapping set, open it.
* If not, ask the user for such a mapping set, and if provided , note
* it in the ecore model.
*/
private MappedStructure getCDAWireMappingSet(String prompt, String annotationKey) throws MapperException
{
boolean newMappingSetChosen = false;
MappedStructure CDAStructure = null;
String wireMappingSetURL = ModelUtil.getMIFAnnotation(ecoreRoot, annotationKey);
// if the ecore model has recorded a wire-form mapping set, use it
if (wireMappingSetURL != null) try
{
String absLocation = FileUtil.absoluteLocation(wireMappingSetURL);
CDAStructure = FileUtil.getMappedStructure(absLocation);
}
// if anything goes wrong, allow the user to nominate a mapping set
catch (Exception ex) {trace(ex.getMessage());wireMappingSetURL = null;}
// if the ecore model has not recorded a good wire-form mapping set, ask the user for one
if (wireMappingSetURL == null)
{
CDAStructure = askUserForMappedStructure(mapperEditor, prompt);
newMappingSetChosen = true;
}
/* if the ecore model has not recorded a wire-form mapping set, but the user has provided one,
* record the one the user provided in the ecore model, and save it */
if ((newMappingSetChosen) && (CDAStructure != null))
{
String absPath = CDAStructure.eResource().getURI().toString();
wireMappingSetURL = FileUtil.resourceLocation(absPath);
ModelUtil.addMIFAnnotation(ecoreRoot, annotationKey, wireMappingSetURL);
saveEcoreModel();
}
return CDAStructure;
}
/**
*
*/
private void orderFullMappingSet() throws MapperException
{
String prompt = "Open generic CDA Mapping set for wire form tag names";
String annotationKey = "wireFormCDA";
MappedStructure CDAStructure = getCDAWireMappingSet(prompt,annotationKey);
// user may not choose a CDA mapping set; in that case do nothing to the full mapping set
if (CDAStructure != null)
{
ElementDef CDARoot = CDAStructure.getRootElement();
ElementDef fullRoot = fullMappings.getRootElement();
// the csv file for CDA tag names is in the same folder as the CDA mapping set. Remove 'file:/' from the front.
String CDALocation = FileUtil.removeFilePrefix(CDAStructure.eResource().getURI().toString());
String CDATagFileLocation = FileUtil.getFolder(CDALocation) + CDATagFileName;
Vector<String> CDATagFileLines = FileUtil.textLines(CDATagFileLocation);
trace("CDA tag name ruleset has : " + CDATagFileLines.size() + " rows.");
Vector<String[]> CDATagRules = parseCDARules(CDATagFileLines);
renameAndOrderSubtree(CDAStructure,CDARoot,fullRoot,CDATagRules);
// reset the wrapper class to be the wrapper class for wire-form CDA
fullMappings.getMappingParameters().setWrapperClass(NHSCDAWireWrapperClass);
}
}
/**
* recursive descent of the full mapping set tree:
* - in the description field, where the cda wire form tag name is different , give the cda tag name
* - order child nodes in the correct cda order
* @param CDAStructure
* @param CDAElDef
* @param fullElDef
* @throws MapperException
*/
private void renameAndOrderSubtree(MappedStructure CDAStructure,ElementDef CDAElDef,
ElementDef fullElDef,Vector<String[]> CDATagRules)
throws MapperException
{
// trace("renaming " + fullElDef.getPath());
ElementDef cdaEl = CDAElDef;
if (!CDAElDef.isExpanded()) cdaEl = CDAStructure.getStructureDefinition().typeStructure(CDAElDef.getType());
if (cdaEl == null) throw new MapperException("Cannot find structure for node "
+ CDAElDef.getName() + " of type " + CDAElDef.getType());
cdaEl.setName(CDAElDef.getName());
String parentPath = fullElDef.getPath();
// collect available CDA tag names in case the rules give no candidates
String availableCDANames = "";
for (Iterator<ElementDef> it = cdaEl.getChildElements().iterator();it.hasNext();)
{
ElementDef candidateChild = it.next();
String CDAName = candidateChild.getName();
availableCDANames = availableCDANames + CDAName + ", ";
}
// nodes not recognised in CDA, but which the user chooses to retain
Vector<ElementDef> userRetainedNodes = new Vector<ElementDef>();
/* go over child nodes in full mapping set, finding CDA node names where necessary,
* and recursing when you find a unique CDA tag name. */
for (Iterator<ElementDef> it = fullElDef.getChildElements().iterator(); it.hasNext();)
{
ElementDef fullChild = it.next();
String childName = fullChild.getName();
// if the child node does not have a CDA node name, find the right name for it
ElementDef cdaChild = cdaEl.getNamedChildElement(childName);
if (cdaChild == null)
{
Vector<String[]> fixedValues = getObjectMappingValueConditions(fullChild);
Hashtable<String,String> cdaNames = NHS_CDA_TagRuleInterpreter.getCDATagNames(childName, parentPath, fixedValues,CDATagRules);
if (cdaNames.size() == 0)
{
String message = "The CDA naming ruleset suggested no candidate CDA tag names for node '"
+ childName + "' as a child of CDA node '" + cdaEl.getName()
+ "'. Available CDA child nodes are: " + availableCDANames + "; Retain node in any case?";
boolean retain = WorkBenchUtil.askConfirm("Warning", message);
if (retain) userRetainedNodes.add(fullChild);
System.out.println(message);
}
else cdaChild = uniqueCDAChild(cdaEl,cdaNames,fullChild,userRetainedNodes);
if (cdaChild != null)
{
fullChild.setDescription(CDA_NAME + cdaChild.getName());
//trace("Gave CDA name '" + cdaChild.getName() + "' to node '" + fullChild.getName() + "'");
}
}
// only recurse to child subtree if a unique CDA node has been found
if (cdaChild != null) renameAndOrderSubtree(CDAStructure,cdaChild,fullChild,CDATagRules);
}
// order child nodes to match the normative CDA order
EList<ElementDef> orderedChildEls = new BasicEList<ElementDef>();
// first include nodes which were not matched but which the user chose to retain
for (Iterator<ElementDef> ix = userRetainedNodes.iterator(); ix.hasNext();) orderedChildEls.add(ix.next());
// then CDA ordered nodes
for (Iterator<ElementDef> iu = cdaEl.getChildElements().iterator(); iu.hasNext();)
{
ElementDef cdaChild = iu.next();
String cdaName = cdaChild.getName();
// there may be several elements in the full message with the same CDA name, e.g. 'entry'; add them all and count them
int matchingElementsFound = 0;
for (Iterator<ElementDef> iv = fullElDef.getChildElements().iterator(); iv.hasNext();)
{
ElementDef fullChild = iv.next();
String desc = fullChild.getDescription();
int len = CDA_NAME.length();
if ((fullChild.getName().equals(cdaName)) |
((desc != null) && (desc.length() > len)
&& (desc.startsWith(CDA_NAME))
&& (desc.substring(len).equals(cdaName))))
{
orderedChildEls.add(fullChild);
matchingElementsFound++;
}
}
if (matchingElementsFound > 1) System.out.println("Found " + matchingElementsFound + " elements with CDA name '" + cdaName + "'");
}
// reset the list of child elements to the new order
fullElDef.eSet(MapperPackage.eINSTANCE.getElementDef_ChildElements(), orderedChildEls);
}
/**
*
* @param cdaEl a CDA element
* @param cdaNames a list of possible CDA names, which may have repeats
* @return
* @throws MapperException
*/
private ElementDef uniqueCDAChild(ElementDef cdaEl, Hashtable<String,String> cdaNames,
ElementDef fullChild, Vector<ElementDef> userRetainedNodes) throws MapperException
{
ElementDef cdaChild = null;
int found = 0;
String foundNames = "";
// try out all CDA child node names against all names suggested by the rules
String CDAChildNames = "";
for (Iterator<ElementDef> it = cdaEl.getChildElements().iterator();it.hasNext();)
{
ElementDef candidateChild = it.next();
String CDAName= candidateChild.getName();
CDAChildNames = CDAChildNames + CDAName + " ";
// try to match this CDA child name against any of the candidates from the rules
if (cdaNames.get(CDAName) != null)
{
found++;
cdaChild = candidateChild;
foundNames = foundNames + CDAName + ", ";
}
}
// write a warning and return null if there is not a unique matching CDA name
if (found != 1)
{
String ruleNumbers = "";
for (Enumeration<String> en = cdaNames.elements(); en.hasMoreElements();) ruleNumbers = ruleNumbers + en.nextElement() + " ";
String message = ("There are " + found + " matches between the child nodes of CDA node '"
+ cdaEl.getName() + "' and the " + cdaNames.size()
+ " names suggested for node '" + fullChild.getName() + "' by rules " + ruleNumbers + ".");
if (found > 1) message = message + " These matches are: " + foundNames + ".";
message = message + " Candidates from rules are: " + GenUtil.singleKeyString(cdaNames) + ".";
message = message + " CDA child node names are " + CDAChildNames + ". Retain node in any case?";
boolean retain = WorkBenchUtil.askConfirm("Warning", message );
if (retain) userRetainedNodes.add(fullChild);
System.out.println(message);
cdaChild = null;
}
return cdaChild;
}
/**
*
* @param fullChild a node in the mapping set
* @return details of all mapping conditions on object mappings on the node
*/
private Vector<String[]> getObjectMappingValueConditions(ElementDef fullChild)
{
Vector<String[]> conditions = new Vector<String[]>();
/* pick up conditions from object mappings on this node and all ancestor nodes,
* for ancestors taking off the path to this node */
String takeFromPaths = "";
EObject ancestor = fullChild;
while (ancestor instanceof ElementDef)
{
ElementDef elAncestor = (ElementDef)ancestor;
addToFixedValueConditions(elAncestor,conditions,takeFromPaths ,"");
takeFromPaths = elAncestor.getName() + "/" + takeFromPaths;
ancestor = ancestor.eContainer();
}
return conditions;
}
/**
* add to the Vector of fixed value conditions, from the object mappings on this node.
* if addToPaths is non-empty, add it on the front of every path
* if takeFromPaths is non-empty, only include paths from which it can be taken away.
* @param elDef
* @param conditions
* @param takeFromPaths
* @param addToPaths
*/
private void addToFixedValueConditions(ElementDef elDef, Vector<String[]> conditions, String takeFromPaths, String addToPaths)
{
NodeMappingSet nms = elDef.getNodeMappingSet();
if (nms != null) for (Iterator<ObjMapping> it = nms.getObjectMappings().iterator(); it.hasNext();)
{
ObjMapping om = it.next();
for (Iterator<MappingCondition> iu = om.getMappingConditions().iterator();iu.hasNext();)
{
MappingCondition mc = iu.next();
if (mc instanceof ValueCondition)
{
ValueCondition vc = (ValueCondition)mc;
/* always add to the front of paths, even if addToPaths is empty
* addToPaths and takeFromPaths are never both non-empty. */
String path = addToPaths + vc.getLeftPath();
String[] cond = new String[2];
cond[0] = path;
cond[1] = vc.getRightValue();
if (takeFromPaths.equals("")) conditions.add(cond);
// if takeFromPaths is non-empty, the path must start with it
else if (path.startsWith(takeFromPaths))
{
cond[0] = path.substring(takeFromPaths.length());
conditions.add(cond);
}
}
}
}
}
public static Vector<String[]> parseCDARules(Vector<String> CDATagFileLines) throws MapperException
{
Vector<String[]> rules = new Vector<String[]>();
// halt if column headers have changed
String expectedFirstLine = NHS_CDA_TagRuleInterpreter.TAG_RULE_HEADER_ROW;
if (!expectedFirstLine.equals(CDATagFileLines.get(0)))
throw new MapperException("Column headers in CDA Tag Name Rule file have changed, from '"
+ expectedFirstLine + "' to '" + CDATagFileLines.get(0) + "'");
StringTokenizer header = new StringTokenizer(CDATagFileLines.get(0),",");
int columns = header.countTokens();
// first line is column headers and is not used other than for checking
for (int i = 1; i < CDATagFileLines.size(); i++)
{
String[] ruleLine = FileUtil.oldParseCSVLine(columns, CDATagFileLines.get(i));
rules.add(ruleLine);
}
return rules;
}
//------------------------------------------------------------------------------------------------------------
// make and save a thin mapping set (full mapping set without all its fixed value conditions)
//------------------------------------------------------------------------------------------------------------
private void makeThinMappingSet()
{
// make the thin mapping set as a copy of the full mapping set (which has already been saved)
MappedStructure thinMappings = fullMappings;
URI fullURI = thinMappings.eResource().getURI();
thinMappings.eResource().setURI(makeThinURI(fullURI));
// recursively thin out the thin mapping set
thinOutMappings(thinMappings.getRootElement());
// save the thin mapping set
FileUtil.saveResource(thinMappings.eResource());
}
/**
* thin out the value conditions on all object mappings, retaining only value conditions to
* extension attributes
* @param node
*/
private void thinOutMappings(ElementDef node)
{
NodeMappingSet nms = node.getNodeMappingSet();
if (nms != null) for (Iterator<ObjMapping> it = nms.getObjectMappings().iterator();it.hasNext();)
{
ObjMapping om = it.next();
// build up a new list of mapping conditions, containing only value conditions to extension attributes
EList<MappingCondition> newConditions = new BasicEList<MappingCondition>();
for (Iterator<MappingCondition> iu = om.getMappingConditions().iterator(); iu.hasNext();)
{
MappingCondition mc = iu.next();
if (mc instanceof ValueCondition)
{
ValueCondition vc = (ValueCondition)mc;
if (vc.getLeftPath().endsWith("extension")) newConditions.add(vc);
}
else newConditions.add(mc);
}
// reset the list of mapping conditions
om.eSet(MapperPackage.eINSTANCE.getMapping_MappingConditions(), newConditions);
}
// recursion to child nodes
for (Iterator<ElementDef> it = node.getChildElements().iterator(); it.hasNext();)
thinOutMappings(it.next());
}
private URI makeThinURI(URI fullURI)
{
String fullString = fullURI.toString();
String fullEnd = "_full.mapper";
String thinString = fullString.substring(0,fullString.length() - fullEnd.length()) + "_thin.mapper";
return URI.createURI(thinString);
}
//-------------------------------------------------------------------------------------
// JavaDoc annotations for EMF-generated code
//-------------------------------------------------------------------------------------
private String javaDocURI = "http://www.eclipse.org/emf/2002/GenModel";
private String javaDocKey = "documentation";
/**
* add annotations to make javadoc for EMF generated java classes
* @param fullMappings
* @param topPackage
*/
private void addJavaDocAnnotations(MappedStructure fullMappings,EPackage topPackage) throws MapperException
{
ElementDef rootNode = fullMappings.getRootElement();
String path = null;
String templateId = null;
makeJavaDocAnnotations(rootNode,topPackage,path,templateId);
}
/**
* recursive descent of the full mapping set, making JavaDoc annotations on the simple class model
* @param node
* @param topPackage
* @param path CDA path to the node
* @param templateId latest template id encountered along the path
*/
private void makeJavaDocAnnotations(ElementDef node,EPackage topPackage,String path, String templateId) throws MapperException
{
// calculate next path
String nodeName = wireTagName(node);
String newPath = nodeName;
if (path != null) newPath = path + "/" + nodeName;
String newTemplateId = templateId;
// find any object mapping on this node, and annotate the class in the Ecore model
NodeMappingSet nms = node.getNodeMappingSet();
if (nms != null)
{
// annotate the class represented on this node
for (Iterator<ObjMapping> it = nms.getObjectMappings().iterator(); it.hasNext();)
{
ObjMapping om = it.next();
if (getTemplateId(om) != null) newTemplateId = getTemplateId(om);
EClass mappedClass = getMappedSimpleClass(topPackage, om.getMappedPackage(),om.getMappedClass());
addDocumentation(mappedClass,mappingDocString(node,om, newPath, newTemplateId));
}
// annotate the association represented on this node
for (Iterator<AssocMapping> it = nms.getAssociationMappings().iterator(); it.hasNext();)
{
AssocMapping am = it.next();
AssocEndMapping aem1 = am.getMappedEnd1();
AssocEndMapping aem2 = am.getMappedEnd2();
EClass parentClass = getMappedSimpleClass(topPackage, aem1.getMappedPackage(),aem1.getMappedClass());
EStructuralFeature ref = parentClass.getEStructuralFeature(aem2.getMappedRole());
if (ref == null) throw new MapperException("cannot find association " + aem2.getMappedRole() + " of class " + aem1.getMappedClass());
String docString = "<p>Represented at CDA node: <b>" + newPath + "</b></p>";
addDocumentation(ref,docString);
}
// document any property mappings to the pseudo-attribute 'textContent' on this element
documentPropertyMappings(nms, newPath,topPackage,newTemplateId);
}
// document property mappings on attribute nodes
for (Iterator<AttributeDef> it = node.getAttributeDefs().iterator();it.hasNext();) documentAttribute(it.next(),newPath,topPackage,newTemplateId);
// recursion to child nodes
for (Iterator<ElementDef> it = node.getChildElements().iterator();it.hasNext();) makeJavaDocAnnotations(it.next(),topPackage, newPath, newTemplateId);
}
/**
*
* @param topPackage
* @param packageName
* @param className
* @return
* @throws MapperException
*/
private EClass getMappedSimpleClass(EPackage topPackage, String packageName,String className) throws MapperException
{
EPackage thePackage = null;
for (Iterator<EPackage> it = topPackage.getESubpackages().iterator();it.hasNext();)
{
EPackage next = it.next();
if (next.getName().equals(packageName)) thePackage = next;
}
if (thePackage == null) throw new MapperException("Cannot find package " + packageName);
EClass result = (EClass)thePackage.getEClassifier(className);
if (result == null) throw new MapperException("Cannot find class " + className + " in package " + packageName);
return result;
}
/**
* document any property mappings on Attribute nodes
* @param attDef
* @param path
* @param topPackage
* @throws MapperException
*/
private void documentAttribute(AttributeDef attDef, String path, EPackage topPackage, String templateId) throws MapperException
{
String attPath = path + "/@" + attDef.getName();
NodeMappingSet nms = attDef.getNodeMappingSet();
if (nms != null) documentPropertyMappings(nms, attPath, topPackage, templateId);
}
/**
* document property mappings on ElementDef or AttributeDef nodes
* @param nms
* @param path
* @param topPackage
* @throws MapperException
*/
private void documentPropertyMappings(NodeMappingSet nms, String path, EPackage topPackage, String templateId) throws MapperException
{
for (Iterator<PropMapping> it = nms.getPropertyMappings().iterator(); it.hasNext();)
{
PropMapping pm = it.next();
EClass mappedClass = getMappedSimpleClass(topPackage, pm.getMappedPackage(), pm.getMappedClass());
EStructuralFeature mappedFeat = mappedClass.getEStructuralFeature(pm.getMappedProperty());
if (mappedFeat == null) throw new MapperException("Cannot find mapped property " + pm.getMappedClass() + ":" + pm.getMappedProperty());
if (mappedFeat instanceof EAttribute)
{
EAttribute mappedProp = (EAttribute)mappedFeat;
String lastLink = "";
StringTokenizer st = new StringTokenizer(path,"/");
while (st.hasMoreTokens())
{
String link = st.nextToken();
if (!link.startsWith("@")) lastLink = link;
}
String docString = "";
if (mappedProp.getName().equals(POSITION_PROPERTY_NAME))
docString = "<p>The ordinal position 1..N of the '" + lastLink
+ "' node beneath its parent node</p>";
else docString = "<p>Represented at CDA node: <b>" + path + "</b></p>";
addDocumentation(mappedProp,docString);
}
else
{
WorkBenchUtil.showMessage("Javadoc error", "Property "
+ pm.getMappedProperty() + " of class " + pm.getMappedClass() + " is also an association.");
}
}
}
/**
*
* @param om
* @return any template id on this object mapping
*/
private String getTemplateId(ObjMapping om)
{
String templateId = null;
for (Iterator<MappingCondition> it = om.getMappingConditions().iterator();it.hasNext();)
{
MappingCondition mc = it.next();
if (mc instanceof ValueCondition)
{
ValueCondition vc = (ValueCondition)mc;
String vPath = vc.getLeftPath();
if (vPath.equals("npfitlc:contentId/@extension")) templateId = vc.getRightValue();
}
}
return templateId;
}
/**
*
* @param templateId
* @return a link to the DMS
* @throws MapperException
*/
private String dmsLink(String templateId) throws MapperException
{
String href = "../../../../Domains/";
if (templateId == null)
{
// FIXME - need to generalise to other CDA profiles
String noTemplate = "NonCodedCDADocument/Tabular%20View/POCD_HD010011GB02-NoEdit.htm#ClinicalDocument";
href = href + noTemplate;
}
else
{
StringTokenizer st = new StringTokenizer(templateId,"#");
if (st.countTokens() != 2) throw new MapperException("Unexpected form of template id: " + templateId);
href = href + "Templates/Tabular%20View/" + st.nextToken() + "-NoEdit.htm#" + st.nextToken();
}
return ("; see <a href='" + href + "'<b>DMS</b></a>");
}
/**
* preface to the documentation on a Java class or feature, when the message name is defined.
* An erxtra paragraph with the message name in bold - so that when one class or feature is
* used in several messages, the documentation for each is distinguished.
* @return
*/
private String docPreface()
{
String preface = "";
if (messageName() != null) preface = "<p>In message <b>'" + messageName() + "'</b></p>";
return preface;
}
/**
*
* @param node
* @param om
* @param path
* @return
*/
private String mappingDocString(ElementDef node, ObjMapping om, String path, String templateId) throws MapperException
{
// provide a link to the DMS, only for NHS CDAs.
String docString = "<p>Represented at CDA node: <b>" + path + "</b>";
if (isNHSMIF()) docString = docString + dmsLink(templateId);
docString = docString + "</p>";
String templateString = "";
if (templateId != null) templateString = "<p>Template: " + templateId + "</p>";
String fixedValueString = "";
boolean foundFixedValue = false;
for (Iterator<MappingCondition> it = om.getMappingConditions().iterator();it.hasNext();)
{
MappingCondition mc = it.next();
if (mc instanceof ValueCondition)
{
ValueCondition vc = (ValueCondition)mc;
String vPath = vc.getLeftPath();
if ( (!vPath.endsWith("templateId/@root"))
&& (!vPath.endsWith("templateId/@extension"))
&& (!vPath.equals("npfitlc:contentId/@root")))
{
// start a list of fixed values
if (!foundFixedValue) {fixedValueString = "<p>Fixed Values (required or created) in CDA XML:</p><ul>";}
// next fixed value
fixedValueString = fixedValueString + "<li>'" + vc.getRightValue() + "' at path <b>" + truePath(node, vPath) + "</b></li>";
foundFixedValue = true;
}
}
}
// end any list of fixed values
if (foundFixedValue) fixedValueString = fixedValueString + "</ul>";
return docString + templateString + fixedValueString + "<p> </p>";
}
/**
* convert tag names to wire tag names in a path to a fixed value
* @param node
* @param vPath
* @return
*/
private String truePath(ElementDef node, String vPath)
{
String truePath = "";
ElementDef current = node;
StringTokenizer st = new StringTokenizer(vPath,"/",true);
while (st.hasMoreTokens())
{
String step = st.nextToken();
if (step.startsWith("@")) truePath = truePath + step;
else if (step.equals("/")) truePath = truePath + step;
else
{
if (current != null) current = current.getNamedChildElement(step);
if (current == null) truePath = truePath + step;
else truePath = truePath + wireTagName(current);
}
}
return truePath;
}
/**
*
* @param mappedEobject
* @param docString
*/
private void addDocumentation(EModelElement mappedEobject,String docString)
{
EAnnotation ann = mappedEobject.getEAnnotation(javaDocURI);
// if this is the first documentation for this class or feature, add it with the preface defining the message
if (ann == null)
{
ann = EcoreFactory.eINSTANCE.createEAnnotation();
ann.setSource(javaDocURI);
ann.getDetails().put(javaDocKey, docPreface() + docString);
mappedEobject.getEAnnotations().add(ann);
}
// append any subsequent documentation on the same class or feature
else if (ann != null)
{
String previousDoc = ann.getDetails().get(javaDocKey);
String newDoc = previousDoc + docString;
ann.getDetails().put(javaDocKey, newDoc);
}
}
/**
*
* @param elDef
* @return
*/
private String wireTagName(ElementDef elDef)
{
String wireName = elDef.getName();
String desc = elDef.getDescription();
if ((desc != null) && (desc.startsWith(CDA_NAME))) wireName = desc.substring(CDA_NAME.length());
return wireName;
}
//-------------------------------------------------------------------------------------
// Saving changes
//-------------------------------------------------------------------------------------
private void saveEcoreModel()
{
FileUtil.saveResource(ecoreRoot.eResource());
}
private void trace(String s) {if (tracing) System.out.println(s);}
}