package com.openMap1.mapper.health.actions; import java.io.IOException; import java.util.Hashtable; import java.util.Iterator; import java.util.Vector; import org.eclipse.emf.ecore.EAttribute; import org.eclipse.emf.ecore.EClass; import org.eclipse.emf.ecore.EObject; 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.IAction; import org.eclipse.ui.IObjectActionDelegate; import org.w3c.dom.Attr; import org.w3c.dom.Element; import com.openMap1.mapper.AssocEndMapping; import com.openMap1.mapper.AssocMapping; import com.openMap1.mapper.FixedPropertyValue; import com.openMap1.mapper.MappedStructure; import com.openMap1.mapper.MapperFactory; import com.openMap1.mapper.MapperPackage; import com.openMap1.mapper.ObjMapping; import com.openMap1.mapper.actions.MakeITSMappingsAction; import com.openMap1.mapper.actions.MapperActionDelegate; import com.openMap1.mapper.core.MapperException; import com.openMap1.mapper.presentation.MapperEditor; import com.openMap1.mapper.structures.ITSAssociation; import com.openMap1.mapper.structures.MapperWrapper; import com.openMap1.mapper.util.FileUtil; import com.openMap1.mapper.util.ModelUtil; import com.openMap1.mapper.util.XMLUtil; import com.openMap1.mapper.views.ClassModelView; import com.openMap1.mapper.views.FeatureView; import com.openMap1.mapper.views.LabelledEClass; import com.openMap1.mapper.views.WorkBenchUtil; public class ITSMappingsFrom2ExamplesAction extends MapperActionDelegate implements IObjectActionDelegate{ private MapperWrapper wrapper; // wrapper class and package as defined in a mapping set - e.g 'com.openMap1.mapper.converters.NHS_CDA_Converter' private String wrapperClassName = null; private MapperEditor mapperEditor; private MappedStructure mappedStructure; private ClassModelView classModelView; private Hashtable<String,String> universalFixedValues; private boolean includeAllTextNodes = true; public ITSMappingsFrom2ExamplesAction() { super(); } public void run(IAction action) { /* Ensure that the mapping set showing in the editor is the one which the user * right-clicked - so you alter the correct mapping set and Ecore model */ OpenMapperEditor(selection); classModelView = WorkBenchUtil.getClassModelView(false); if (classModelView != null) try { String mappingSetURIString = classModelView.mappingSetURI().toString(); mapperEditor = WorkBenchUtil.getMapperEditor(mappingSetURIString); if (mapperEditor != null) { Element XMLRoot2 = getExampleMessageAndWrapIn("Select Annotated Message Instance", true); if (XMLRoot2 == null) return; Element XMLRoot1 = getExampleMessageAndWrapIn("Select Unmodified Message Instance", false); if (XMLRoot1 != null) { // collect fixed values, to be applied on all nodes wherever they appear in the instances universalFixedValues = new Hashtable<String,String>() ; trace("Collect Universal Fixed Values"); collectUniversalFixedValues(XMLRoot1,XMLRoot2); // make annotations on the Ecore model from the two example messages trace("Annotate Ecore model"); annotateEcoreModel(XMLRoot1,XMLRoot2); // apply automatic flattening rules, except where overridden LabelledEClass topClass = classModelView.topLabelledEClass(); trace("Apply flattening rules"); applyFlatteningRules(topClass,XMLRoot2); // use the altered Ecore model to make a mapping set trace("Make mapping set"); new MakeITSMappingsAction().run(); mappedStructure = mappedStructure(); // add fixed values defined in the RMIM as requested in the example. 'null' means no previous class to link to and no ref name trace("Add fixed attribute values"); addAllFixedValues(topClass, mappedStructure, null, null); // save the updated mapping set trace("Save mapping set"); FileUtil.saveResource(mappedStructure.eResource()); } } } catch (Exception ex) {WorkBenchUtil.showMessage("Error",ex.getMessage());} } /** * get the root element of one of the two example messages, and if there is a wrapper transform, * apply the in-wrapper transform to it * @param dialogTitle * @param findWrapperClass * @return * @throws MapperException */ private Element getExampleMessageAndWrapIn(String dialogTitle, boolean findWrapperClass) throws MapperException { // get the root element of the unmodified or annotated XML instance String[] exts = {"*.xml"}; String path = FileUtil.getFilePathFromUser(targetPart,exts,dialogTitle,false); if (path.equals("")) return null; Element XMLRoot = XMLUtil.getRootElement(path); if (XMLRoot == null) throw new MapperException("Could not open requested file"); /* If the example message uses a wrapper class, the annotated example message should define * the location in the workspace of the mapping set that identifies the wrapper class, * in an attribute "mappingSet" on its root element */ if (findWrapperClass) { wrapper = null; String mappingSetURI = XMLRoot.getAttribute("mappingSet"); if ((mappingSetURI != null) && (!mappingSetURI.equals(""))) try { String absLoc = FileUtil.absoluteLocation(mappingSetURI); MappedStructure otherMS = FileUtil.getMappedStructure(absLoc); // get an instance of the wrapper class; spare argument is the root element name String rootName = ""; if (otherMS.getRootElement() != null) rootName = otherMS.getRootElement().getName(); wrapper = otherMS.getWrapper(rootName); if (wrapper != null) wrapperClassName = otherMS.getMappingParameters().getWrapperClass(); } catch (IOException ex) {throw new MapperException("Cannot find mapping set at '" + mappingSetURI + "'");} } if (wrapper != null) return wrapper.transformIn(XMLRoot).getDocumentElement(); else return XMLRoot; } /** * * @param XMLRoot1 * @param XMLRoot2 * @throws MapperException */ private void annotateEcoreModel(Element XMLRoot1,Element XMLRoot2) throws MapperException { String rootError = "Root element tag name of XML instance does not match top class of Ecore model: "; LabelledEClass topClass = classModelView.topLabelledEClass(); String path = topClass.eClass().getName(); if (!(XMLRoot1.getLocalName().equals(path))) throw new MapperException(rootError + path); if (!(XMLRoot2.getLocalName().equals(path))) throw new MapperException(rootError + path); if (wrapperClassName != null) ModelUtil.addMIFAnnotation(topClass.eClass(), "wrapperClass", wrapperClassName); annotateModel(topClass, XMLRoot1,XMLRoot2); } /** * * @param val * @return true if this value has been marked to be a fixed value wherever it occurs in the example */ private boolean universalFixedValue(String val) {return (universalFixedValues.get(val) != null);} /** * recursive descent of two example messages, using the differences between them to annotate the Ecore model, * so it can be used to make a mapping set for a simplified XML * @param theClass class in the ECore model corresponding to current nodes of the two example messages * @param XMLRoot1 current element in the unmodified example message * @param XMLRoot2 current element in the annotated example message * @return true if there are any attributes marked to be included in the simplified message, * in the subtree below this class */ private boolean annotateModel(LabelledEClass theClass, Element el1,Element el2) throws MapperException { boolean hasUsedAttributes = false; // look for any attributes which have different values in the two examples, and mark them in the model for (int a = 0; a < el1.getAttributes().getLength(); a++) { Attr att = (Attr)el1.getAttributes().item(a); String val1= att.getValue(); String val2= el2.getAttribute(att.getName()); if ((!val1.equals(val2))|universalFixedValue(val1)) { if (markAttributeUsedOrFixed(theClass,att.getName(), val1, val2)) hasUsedAttributes = true; } } /* for elements with no descendant elements, the text content in the two examples may differ. * If so, make sure there is a 'textContent' EAttribute in the Ecore model, and mark it as used, * possibly with renaming */ if (XMLUtil.childElements(el1).size() == 0) { String val1 = XMLUtil.getText(el1); String val2 = XMLUtil.getText(el2); if ((!val1.equals(val2))|universalFixedValue(val1)) { // add a 'textContent' EAttribute to the class if it does not yet exist addTextContentAttribute(theClass.eClass()); if (markAttributeUsedOrFixed(theClass,"textContent", val1, val2)) hasUsedAttributes = true; } } /* Classes reached by a <text> element should have a 'textContent' attribute which is * marked as used, if 'includeAllTextNodes' is true. */ if ((XMLUtil.getLocalName(el1).equals("text")) && includeAllTextNodes) { addTextContentAttribute(theClass.eClass()); if (markAttributeUsedOrFixed(theClass,"textContent", "a", "a#")) hasUsedAttributes = true; } // iterate over child elements, in step Vector<Element> c1 = XMLUtil.childElements(el1); Vector<Element> c2 = XMLUtil.childElements(el2); for (int c = 0; c < c1.size(); c++) { Element cEl1 = c1.get(c); Element cEl2 = c2.get(c); LabelledEClass theChild = theClass.getNamedAssocChild(cEl1.getLocalName()); if (theChild != null) { if (annotateModel(theChild,cEl1,cEl2)) hasUsedAttributes = true; } // ED and ANY nodes (eg <text>) can have any kind of child node else if (theChild == null) { if (!canHaveAnyChild(theClass)) throw new MapperException("Cannot find child labelled EClass '" + cEl1.getLocalName() + "' from class '" + theClass.eClass().getName() + "'"); } } if (hasUsedAttributes) mark_CV_II_Attributes(theClass); return hasUsedAttributes; } /** * This LabelledEClass is going to be mapped, as it has some used attributes below it. * If it has any EReferences marked with the annotation 'fixed att value' they should * lead only to an II or CV data type object. Mark the 'extension' or 'code' attribute * of that data type object as having a fixed value; and mark the EReference as used, so * that the child class and the association get mapped. * @param theClass */ private void mark_CV_II_Attributes(LabelledEClass theClass) throws MapperException { for (Iterator<EReference> ir = theClass.eClass().getEAllReferences().iterator(); ir.hasNext();) { EReference ref = ir.next(); String fixedVal = ModelUtil.getEAnnotationDetail(ref,"fixed att value"); if (fixedVal != null) { LabelledEClass child = theClass.getNamedAssocChild(ref.getName()); if (child != null) { String val1=fixedVal; String val2= val1+ "#f"; String nextCName = child.eClass().getName(); // this is what NHS template RMIMs mean when they put 'fixedValue' on a data type class: II or CV or BL if (nextCName.equals("II")) markAttributeUsedOrFixed(child,"extension",val1,val2); else if (nextCName.equals("CV")) markAttributeUsedOrFixed(child,"code",val1,val2); else if (nextCName.equals("BL")) markAttributeUsedOrFixed(child,"value",val1,val2); else throw new MapperException("Cannot place a fixed attribute value on class '" + nextCName + "' from class " + theClass.eClass().getName() + " in package " + theClass.eClass().getEPackage().getName()); child.markAsUsedInMicroITS(true); // mark the EReference to this data type child } } } } /** * add a 'textContent' EAttribute to the class if it does not yet exist * @param aClass */ private void addTextContentAttribute(EClass aClass) { String attName = "textContent"; EAttribute eText = (EAttribute)aClass.getEStructuralFeature(attName); if (eText == null) { eText = EcoreFactory.eINSTANCE.createEAttribute(); eText.setName(attName); eText.setLowerBound(0); eText.setEType(EcorePackage.eINSTANCE.getEString()); aClass.getEStructuralFeatures().add(eText); } } /** * mark an EAttribute of a class as to be used in the simple ITS, * possibly with renaming; and mark all associations on the path down * top the EAttribute as used. * @param theClass * @param attName * @param val1 * @param val2 * @throws MapperException */ private boolean markAttributeUsedOrFixed(LabelledEClass theClass,String attName, String val1, String val2) throws MapperException { boolean used = false; // find the EAttribute to be affected EAttribute eAtt = (EAttribute)theClass.eClass().getEStructuralFeature(attName); if (eAtt == null) { String warning = "Cannot find Eattribute '" + attName + "' of class '" + theClass.eClass().getName() + "'"; /* sometimes the annotated message will not precisely match the RMIM, * and this should not be a fatal error. For the moment, just write a warning message. */ // throw new MapperException(warning); System.out.println(warning); return false; } /* if the attribute is to be renamed, indicate by '#<new name> added to the value in the altered example * where <newName> is not 'f' or 'fa' */ String newName = ""; if ((val2.length() > val1.length()) && (val2.startsWith(val1))) { String remainder = val2.substring(val1.length()); if (remainder.startsWith("#")) newName = remainder.substring(1); } // some additions mean a fixed value; annotation to add a fixed property value to the object mapping if ((newName.equals("f"))|(newName.equals("fa"))|(universalFixedValue(val1))) { FeatureView.addMicroITSAnnotation(eAtt, "fixed:" + theClass.getPath(), val1); } /* additions which do not mean a fixed value; annotation to add a property mapping to an attribute * with the existing name or name 'newName' */ else { String annotation = "T:" + newName; FeatureView.addMicroITSAnnotation(eAtt, theClass.getPath(), annotation); used = true; } /* If an attribute is used for a non-fixed value, for all ancestor EAssociations, set the used flag. * Ascend the tree until you find a flag that is already set , * or you reach the top of the tree */ LabelledEClass current = theClass; if (used) while (current != null) { boolean oldUsedState = current.isMarkedUsedInMicroITS(); if (!oldUsedState) // need to mark and ascend { current.markAsUsedInMicroITS(true); current = current.parent(); // null if current was top of the tree } else if (oldUsedState) current = null; // need go no further } return used; } /** * The automatic flattening rules are that any EReference which is marked as used * and has max multiplicity 1, gets flattened, unless it has been marked * with an attribute flatten="no" or has a textContent attribute */ private void applyFlatteningRules(LabelledEClass theClass, Element el) throws MapperException { for (Iterator<Element> it = XMLUtil.childElements(el).iterator();it.hasNext();) { Element childEl = it.next(); String tagName = childEl.getLocalName(); EStructuralFeature sRef = theClass.eClass().getEStructuralFeature(tagName); if ((sRef != null) && (sRef instanceof EReference)) { EReference ref = (EReference)sRef; ITSAssociation itsa = FeatureView.getITSAssociation(ref, theClass.getPath()); // do nothing , and recurse no further, unless this association is marked as having used attributes below it if (itsa.attsIncluded()) { /* automatic flattening rule. Flatten if max multiplicity is 1, * and if the child EClass has no textContent attribute */ EClass child = (EClass)ref.getEType(); boolean collapsed = ((ref.getUpperBound() == 1) && (child.getEStructuralFeature("textContent")== null)); // manual override of flattening rules String flatten = childEl.getAttribute("flatten"); if (flatten.equals("no")) collapsed = false; if (flatten.equals("yes")) collapsed = true; itsa.setCollapsed(collapsed); String rename = childEl.getAttribute("rename"); itsa.setBusinessName(rename); FeatureView.addMicroITSAnnotation(ref, theClass.getPath(), itsa.stringForm()); LabelledEClass theChild = theClass.getNamedAssocChild(tagName); if (theChild != null) applyFlatteningRules(theChild,childEl); // ED nodes (eg <text>) can have any kind of child node else if ((theChild == null) && (!(theClass.eClass().getName().equals("ED")))) { throw new MapperException("Cannot find child labelled EClass '" + tagName + "' from class '" + theClass.eClass().getName() + "'"); } } } // there are all sorts of elements below ED or ANY nodes, whose names we do not know if ((sRef == null) && (!(canHaveAnyChild(theClass)))) throw new MapperException("Cannot find link '" + tagName + "' from class '" + theClass.eClass().getName() + "'"); } } /** * * @param theClass * @return true if the class is allowed to have all sort of child links */ private boolean canHaveAnyChild(LabelledEClass theClass) { String cName = theClass.eClass().getName(); return ((cName.equals("ED"))|(cName.equals("ANY"))); } /** * collect all text values which have been marked to be fixed values * wherever they occur in the instances, by recursive descent * @param el1 * @param el2 */ private void collectUniversalFixedValues(Element el1,Element el2) { // remember any attribute where '#fa' has been added to the value in the annotated example for (int a = 0; a < el1.getAttributes().getLength(); a++) { Attr att = (Attr)el1.getAttributes().item(a); String val1= att.getValue(); String val2= el2.getAttribute(att.getName()); if (val2.equals(val1 + "#fa")) universalFixedValues.put(val1, "1"); } // remember any text content of an element where '#fa' has been added to the value in the annotated example if (XMLUtil.childElements(el1).size() == 0) { String val1 = XMLUtil.getText(el1); String val2 = XMLUtil.getText(el2); if (val2.equals(val1 + "#fa")) universalFixedValues.put(val1, "1"); } // iterate over child elements, in step Vector<Element> c1 = XMLUtil.childElements(el1); Vector<Element> c2 = XMLUtil.childElements(el2); for (int c = 0; c < c1.size(); c++) { Element cEl1 = c1.get(c); Element cEl2 = c2.get(c); collectUniversalFixedValues(cEl1,cEl2); } } /** * recursive descent of the LabelledEClass tree, adding fixed property values * to object mappings wherever requested on the tree * @param theClass current LabelledEClass * @param mappedStructure */ private void addAllFixedValues(LabelledEClass theClass, MappedStructure mappedStructure, ObjMapping previous, String refName) throws MapperException { ObjMapping oMap = linkedMappingForClass(refName,theClass,mappedStructure,previous); if (oMap != null) { // If any EAttributes for the class have fixed values for the association path, add the fixed values String attKey = "fixed:" + theClass.getPath(); for (Iterator<EAttribute> it = theClass.eClass().getEAllAttributes().iterator(); it.hasNext();) { EAttribute att = it.next(); String attName= att.getName(); // general fixed value defined by the RMIM, for any path String fixedVal = ModelUtil.getEAnnotationDetail(att,"fixed value"); // specific fixed value requested for this path on the annotated example instance String fixedVal1 = FeatureView.getMicroITSAnnotation(att, attKey); //specific fixed value takes precedence if (fixedVal1 != null) fixedVal = fixedVal1; if (fixedVal != null) addFixedValue(oMap,attName,fixedVal); } // recurse to the next LabelledEClass, and pick up fixed values of II and CV data types for (Iterator<EReference> it = theClass.eClass().getEAllReferences().iterator(); it.hasNext();) { EReference ref = it.next(); String nextRefName = ref.getName(); LabelledEClass nextClass = theClass.getNamedAssocChild(nextRefName); if (nextClass == null) throw new MapperException("Cannot find Labelled Class by link " + nextRefName + " from class '" + theClass.eClass().getName() + "'"); // recursive step; stops if there is no mapping addAllFixedValues(nextClass,mappedStructure,oMap,nextRefName); // annotations on EReferences set a property of the child data type class String nextCName = nextClass.eClass().getName(); String fixedVal = ModelUtil.getEAnnotationDetail(ref,"fixed att value"); if (fixedVal != null) { ObjMapping nextMap = linkedMappingForClass(nextRefName,nextClass,mappedStructure,oMap); if (nextMap != null) { // this is what NHS template RMIMs mean when they put 'fixedValue' on a data type class if (nextCName.equals("II")) addFixedValue(nextMap,"extension",fixedVal); else if (nextCName.equals("CV")) addFixedValue(nextMap,"code",fixedVal); else if (nextCName.equals("BL")) addFixedValue(nextMap,"value",fixedVal); else throw new MapperException("Cannot place a fixed attribute value on class '" + nextCName + "'"); } } } } } /** * add a fixed property value to an object mapping, * if there is not already one for the same attribute * @param oMap * @param attName * @param fixedVal */ private void addFixedValue(ObjMapping oMap, String attName, String fixedVal) { trace("fixed " + attName + " to " + fixedVal); // do not add a fixed value mapping if there is one already for this attribute boolean hasFixedValue = false; for (Iterator<FixedPropertyValue> iv = oMap.getFixedPropertyValues().iterator();iv.hasNext();) if (iv.next().getMappedProperty().equals(attName)) hasFixedValue = true; if (!hasFixedValue) { FixedPropertyValue fpv = MapperFactory.eINSTANCE.createFixedPropertyValue(); fpv.setMappedProperty(attName); fpv.setFixedValue(fixedVal); fpv.setValueType("string"); oMap.getFixedPropertyValues().add(fpv); } } /** * @param refName * @param theClass * @param ms * @param previous * @return an Object mapping for the class, which is linked to the previous * object mapping by an association mapping with the defined role name, if it exists; null otherwise * If the previous object mapping is null, choose any object mapping to the present class (it is the top class, * so assume there is only one object mapping to it) */ private ObjMapping linkedMappingForClass(String refName, LabelledEClass theClass, MappedStructure ms, ObjMapping previous) throws MapperException { /* if there is a previous object mapping to link to, find what subset of the current class has * an object mapping linked by an association mapping to the previous object mapping, with the correct role name */ String subset = null; if ((previous != null) && (refName != null)) { for (Iterator<EObject> it = ModelUtil.getEObjectsUnder(ms, MapperPackage.eINSTANCE.getAssocMapping()).iterator();it.hasNext();) { AssocMapping am = (AssocMapping)it.next(); for (int e = 0; e < 2; e++) { AssocEndMapping aem = am.getMappedEnd(e); if (aem.getClassSet().equals(previous.getClassSet())) { AssocEndMapping bem = am.getMappedEnd(1-e); if ((bem.getClassSet().className().equals(ModelUtil.getQualifiedClassName(theClass.eClass()))) && (refName.equals(bem.getMappedRole()))) subset = bem.getClassSet().subset(); } } } } // failure - there was a previous object mapping, and there is no association mapping linking it to the current class if ((previous != null) && (subset == null)) return null; // find all object mappings to the class; and if the subset is known, filter by the subset for (Iterator<EObject> it = ModelUtil.getEObjectsUnder(ms, MapperPackage.eINSTANCE.getObjMapping()).iterator();it.hasNext();) { ObjMapping om = (ObjMapping)it.next(); if (om.getClassSet().className().equals(ModelUtil.getQualifiedClassName(theClass.eClass()))) { if (subset == null) return om; if (om.getClassSet().subset().equals(subset)) return om; } } return null; } }