package com.openMap1.mapper.actions; import java.util.Hashtable; import java.util.Iterator; import java.util.Vector; import org.eclipse.core.resources.IFile; import org.eclipse.emf.common.util.EList; import org.eclipse.emf.ecore.EObject; import org.eclipse.jface.action.Action; import org.eclipse.jface.action.IAction; import org.w3c.dom.Attr; import org.w3c.dom.Element; import org.w3c.dom.NamedNodeMap; import org.w3c.dom.Node; import org.w3c.dom.Text; import com.openMap1.mapper.converters.CDAConverter; import com.openMap1.mapper.core.MapperException; import com.openMap1.mapper.presentation.MapperEditor; import com.openMap1.mapper.structures.XSDStructure; import com.openMap1.mapper.util.EclipseFileUtil; import com.openMap1.mapper.util.FileUtil; import com.openMap1.mapper.util.GenUtil; import com.openMap1.mapper.util.XMLOutputFile; import com.openMap1.mapper.util.XMLUtil; import com.openMap1.mapper.views.WorkBenchUtil; import com.openMap1.mapper.AttributeDef; import com.openMap1.mapper.ElementDef; import com.openMap1.mapper.MappedStructure; 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; public class MakeITSInstanceAction extends Action implements IAction{ private MapperEditor mapperEditor; private MappedStructure mappedStructure; /* in expanding un-expanded elements, the maximum number of nested repeats of the same type */ private int MAX_TYPE_REPEATS = 1; /* for elements that can repeat, the number of repeats generated in the xml */ private int MULTI_REPEAT = 1; boolean tracing = true; /* index used to put different attribute values on different string attributes of the skeleton instance */ private int stringIndex = 0; /* index used to put different attribute values on different integer or float attributes of the skeleton instance */ private int numberIndex = 0; /* index to count the number of boolean values 'false' put in the generated instance */ private int booleanIndex = 0; // number of vales copied across from a previous example private int previousValuesUsed = 0; // variable numbers in some previous example, to be retained, keyed by XPath private Hashtable<String,Integer> previousVariablesByPath; // if a previous example occupies some range of variable numbers, this is the lowest new variable number to use, to avoid clashes private int lowestNewVariableNumber = 0; public MakeITSInstanceAction() { super("Make Example Message and Schema"); } /** * Make a skeleton instance of the XML, and a schema (allowing either to be cancelled independently) */ public void run() { mapperEditor = WorkBenchUtil.getOpenMapperEditor(); if (mapperEditor != null) { mappedStructure = WorkBenchUtil.mappingRoot(mapperEditor); if (mappedStructure.getRootElement() != null) try { writeSkeletonInstance(); writeSchema(); } catch(Exception ex) { errorMessage(ex.getMessage()); ex.printStackTrace(); } else errorMessage("No root element in mapping set"); } else errorMessage("No Mapping set currently open in the Mapper Editor"); } //------------------------------------------------------------------------------------------ // Making a skeleton instance //------------------------------------------------------------------------------------------ private void writeSkeletonInstance() throws MapperException { askUserForPreviousExample(); IFile instanceFile = WorkBenchUtil.makeOutputIFile("Skeleton XML Instance", "Choose file name for example message instance"); if (instanceFile != null) { XMLOutputFile outFile = new XMLOutputFile(); ElementDef rootDef = mappedStructure.getRootElement(); Element root = outFile.newElement(rootDef.getName()); addNamespaceAttributes(root); outFile.setTopOut(root); // if simple messages are in the V3 namespace, give it a namespace attribute if (CDAConverter.SIMPLE_MESSAGE_IN_V3_NAMESPACE) root.setAttribute("xmlns", CDAConverter.V3NAMESPACEURI); previousValuesUsed = 0; stringIndex = 0; numberIndex = 0; booleanIndex = 0; // recursive descent, expanding elements as needed String path = ""; fillElement(outFile,rootDef,root,path); int dataValues = previousValuesUsed + stringIndex + numberIndex + booleanIndex; /* Write out the instance. ask the user for a file name. */ EclipseFileUtil.writeOutputResource(outFile.outDoc(),instanceFile,true); WorkBenchUtil.showMessage("Completed", "Wrote message instance with " + dataValues + " arbitrary data values "); } else WorkBenchUtil.showMessage("Cancelled","No message instance written"); } /** * * @throws MapperException */ private void askUserForPreviousExample() throws MapperException { previousVariablesByPath = new Hashtable<String,Integer>(); String[] exts = {"*.xml"}; String filePath = FileUtil.getFilePathFromUser(mapperEditor,exts,"Select Previous example to retain variable numbers",false); if (filePath.equals("")) return; Element root = XMLUtil.getRootElement(filePath); int variables = noteAllVariables(root,""); System.out.println("variables noted: " + variables + "; " + previousVariablesByPath.size() + "." + lowestNewVariableNumber); } /** * recursively note all variables in an XML element and all its descendants * @param el * @param path */ private int noteAllVariables(Element el,String path) { int count = 0; String newPath = path + "/" + XMLUtil.getLocalName(el); // all paths begin with '/' String textContent = XMLUtil.getText(el); if (!textContent.equals("")) {noteVariable(textContent,newPath);count++;} Vector<Element> children = XMLUtil.childElements(el); for (Iterator<Element> it = children.iterator();it.hasNext();) {count = count + noteAllVariables(it.next(),newPath);} NamedNodeMap nn = el.getAttributes(); for (int i = 0; i < nn.getLength();i++) { Node n = nn.item(i); if (n instanceof Attr) { Attr att = (Attr)n; String attPath = newPath + "/@" + att.getName(); String val = att.getValue(); if (!val.equals("")) noteVariable(val,attPath); count++; } } return count; } /** * note the XPath to a number variable in a previous example, and increment the highest number found in the previous example * @param text * @param path */ private void noteVariable(String text, String path) { // text variable value should always be an Integer; ignore silently if not try { Integer val = new Integer(text); int ival = val.intValue(); previousVariablesByPath.put(path, val); if (ival > lowestNewVariableNumber) lowestNewVariableNumber = ival; } catch (Exception ex) {System.out.println("non-Integer value: " + text);} } /** * recursively fill elements of the sample xml instance with distinct data values * @param outFile * @param elDef * @param el */ private void fillElement(XMLOutputFile outFile,ElementDef elDef,Element el, String path) throws MapperException { String newPath = path + "/" + XMLUtil.getLocalName(el); // expand the element definition if necessary ElementDef currentEl = elDef; if (!elDef.isExpanded()) currentEl = mappedStructure.getStructureDefinition().typeStructure(elDef.getType()); if (currentEl == null) currentEl = elDef; // revert if there is no type definition if (typeRepeats(elDef) > MAX_TYPE_REPEATS) currentEl = elDef; // revert if too deep recursion // if the text content of the Element is mapped to something, add text content NodeMappingSet nms = currentEl.getNodeMappingSet(); boolean fillText = ((nms != null) && (nms.getPropertyMappings().size() == 1)); // the element has no attributes or child elements, add text content if ((currentEl.getChildElements().size() == 0) && (currentEl.getAttributeDefs().size() == 0)) fillText = true; if (fillText) { Text tCon = outFile.outDoc().createTextNode(nextDataValue(currentEl,newPath)); el.appendChild(tCon); } // add attributes to the element, with a different value for each attribute for (Iterator<AttributeDef> ia = currentEl.getAttributeDefs().iterator();ia.hasNext();) { AttributeDef ad = ia.next(); String attPath = newPath + "/@" + ad.getName(); String value = nextDataValue(ad,attPath); try {el.setAttribute(ad.getName(),value);} catch (Exception ex) { WorkBenchUtil.showMessage("Error","Failed to write value '" + value + "' to XML attribute '" + ad.getName() + "' of element '" + currentEl.getName() + "'. Check invalid characters in XML attribute name."); } } // add child elements recursively. If more than one is allowed, add MULTI_REPEAT of them. for (Iterator<ElementDef> ie = currentEl.getChildElements().iterator();ie.hasNext();) { ElementDef childDef = ie.next(); int numberOfRepeats = 1; if (childDef.getMaxMultiplicity() == MaxMult.UNBOUNDED) numberOfRepeats = MULTI_REPEAT; for (int repeat = 0; repeat < numberOfRepeats; repeat++) { Element childEl = outFile.newElement(childDef.getName()); el.appendChild(childEl); fillElement(outFile,childDef,childEl, newPath); } } } /** * * @param elDef * @return true if the element is a text node - i.e if its name without namespace is 'text' private boolean isTextNode(ElementDef elDef) { StringTokenizer st = new StringTokenizer(elDef.getName(),":"); String localName = ""; while (st.hasMoreTokens()) localName = st.nextToken(); if (localName.equals("text")) return true; return false; } */ /** * * @param nDef * @param path - the XPTH to the node - not strictly necessary, as it can be got from the NodeDef - * but hthie path is calculated identically for the old instance and the new * @return another distinct data value, of the correct type */ private String nextDataValue(NodeDef nDef,String path) { // if a previous example had a variable at this XPath, reuse that value Integer previousValue = previousVariablesByPath.get(path); if (previousValue != null) { previousValuesUsed++; return previousValue.toString(); } // if a previous example used up a range of variable numbers, start higher for new variables, to avoid duplicates int bigNumber = 11001; // to avoid small number strings if (lowestNewVariableNumber > bigNumber) bigNumber = lowestNewVariableNumber + 1; String value = ""; if (isBooleanValue(nDef)) { value = "false"; booleanIndex++; } else if (isNumberValue(nDef)) { value = new Integer(bigNumber + numberIndex).toString(); numberIndex++; } else { value = "a_" + stringIndex; stringIndex++; } return value; } /** * what the hell - make them all numbers * @param nDef * @return */ private boolean isNumberValue(NodeDef nDef) { boolean isNumber = true; return isNumber; } private boolean isBooleanValue(NodeDef nDef) { boolean isBoolean = false; if ((nDef.getType() != null) && (nDef.getType().equals("boolean"))) isBoolean = true; if (nDef.getName().equals("refusalInd")) isBoolean = true; return isBoolean; } /** * * @param elDef * @return the number of times the type of this ElementDef has occurred in its ancestors */ private int typeRepeats(ElementDef elDef) { int repeats = 0; String type = elDef.getType(); if (type != null) { EObject current = elDef.eContainer(); while (current instanceof ElementDef) { ElementDef parent = (ElementDef)current; String pType = parent.getType(); if ((pType != null)&& (pType.equals(type))) repeats++; current = parent.eContainer(); } } return repeats; } /** * add namespace declarations to the root element of the xml instance * @param root */ private void addNamespaceAttributes(Element root) { if (mappedStructure.getMappingParameters() != null) { EList<Namespace> nsList = mappedStructure.getMappingParameters().getNameSpaces(); if (nsList != null) for (Iterator<Namespace> it = nsList.iterator();it.hasNext();) { Namespace ns = it.next(); String prefix = ns.getPrefix(); String url = ns.getURL(); if (prefix.equals("")) root.setAttribute("xmlns", url); else root.setAttribute("xmlns:" + prefix, url); } } } //------------------------------------------------------------------------------------------ // Making an XML Schema //------------------------------------------------------------------------------------------ /* schemas to be included to define all the types that may be used */ private String[] includeLocations = {"../dt/infrastructureRoot.xsd", "../VocabularySchemas/CDAVocab.xsd", "../dt/datatypeflavours.xsd"}; private String XSDPrefix = "xs"; private Element schemaElement(XMLOutputFile outFile, String elName) { return outFile.NSElement(XSDPrefix, elName, XMLUtil.SCHEMAURI); } /** * used to ensure elements of the same name get different complex types only if they have * different structures beneath them */ private Hashtable<String,ElementDef> complexTypes; private void writeSchema() throws MapperException { /* Ask the user for a location */ IFile instanceFile = WorkBenchUtil.makeOutputIFile("Generate XML Schema", "Choose name for schema"); if (instanceFile != null) { complexTypes = new Hashtable<String,ElementDef>(); // make the root element of the schema XMLOutputFile outFile = new XMLOutputFile(); Element root = schemaElement(outFile,"schema"); root.setAttribute("xmlns:" + XSDPrefix,XMLUtil.SCHEMAURI); root.setAttribute("elementFormDefault", "qualified"); outFile.setTopOut(root); // if simple messages are in the V3 namespace, add the namespace attribute as target namespace if (CDAConverter.SIMPLE_MESSAGE_IN_V3_NAMESPACE) { root.setAttribute("xmlns", CDAConverter.V3NAMESPACEURI); root.setAttribute("targetNamespace", CDAConverter.V3NAMESPACEURI); } ElementDef rootDef = mappedStructure.getRootElement(); // include datatypes schemas, if any attributes have declared types if (attributesHaveTypes(rootDef)) { for (int i = 0; i < includeLocations.length; i++) { Element includeEl = schemaElement(outFile,"include"); includeEl.setAttribute("schemaLocation",includeLocations[i]); root.appendChild(includeEl); } } // declaration for the root element Element declaration = schemaElement(outFile,"element"); declaration.setAttribute("name", rootDef.getName()); root.appendChild(declaration); // make the anonymous complex type of the root element (and recursive descent) makeType(outFile,declaration,rootDef,false); EclipseFileUtil.writeOutputResource(outFile.outDoc(),instanceFile,true); // attach the new schema to the mapping set String schemaLocation = EclipseFileUtil.getResourceLocation(instanceFile); mappedStructure.setStructureURL(schemaLocation); FileUtil.saveResource(mappedStructure.eResource()); // open the schema to work out enumerated simple data types resolveEnumeratedDataTypes(); WorkBenchUtil.showMessage("Completed","XML schema written"); } else WorkBenchUtil.showMessage("Cancelled","No XML schema written"); } /** * * @param elDef * @return true if any of the AttributeDefs in the tree beneath this * ElementDef have types other than "" */ private boolean attributesHaveTypes(ElementDef elDef) { boolean attsHaveTypes = false; for (Iterator<AttributeDef> it = elDef.getAttributeDefs().iterator();it.hasNext();) if (!it.next().getType().equals("")) attsHaveTypes = true; for (Iterator<ElementDef> it = elDef.getChildElements().iterator();it.hasNext();) if (attributesHaveTypes(it.next())) attsHaveTypes = true; return attsHaveTypes; } /** * Recursively make all complex types for this ElementDef and all others * under it in the tree, and attach them to the top element of the output file * * @param outFile the output XML file * @param schemaEl the element the type definition is to be attached to * @param elDef the ElementDef the complex type describes * @param makeTypeName if true, give the complex type a name * @return the complex type name, or null if there is none */ private String makeType(XMLOutputFile outFile, Element schemaEl, ElementDef elDef,boolean makeTypeName) throws MapperException { // if the element already has a type, use that type name String typeName= elDef.getType(); // "" if the element has no pre-defined type // give the type a new name if one is required, and mark the element with the type if ((makeTypeName) && (typeName.equals(""))) { typeName = newComplexTypeName(elDef); elDef.setType(typeName); } if (typeName == null) throw new MapperException("Null type for element " + elDef.getName()); // if the complex type is new, define it if (complexTypes.get(typeName) == null) { complexTypes.put(typeName, elDef); //record use of the type name Element typeEl = schemaElement(outFile,"complexType"); if (!typeName.equals("")) typeEl.setAttribute("name", typeName); schemaEl.appendChild(typeEl); // mixed type if the element represents any properties if (containsText(elDef)) typeEl.setAttribute("mixed","true"); // describe the child elements of the type Element seqEl = schemaElement(outFile,"sequence"); int children = 0; for(Iterator<ElementDef> ie = elDef.getChildElements().iterator();ie.hasNext();) { children++; ElementDef cDef = ie.next(); Element childEl = schemaElement(outFile,"element"); childEl.setAttribute("name",nodeNameForSchema(cDef)); if (cDef.getMaxMultiplicity() == MaxMult.UNBOUNDED) childEl.setAttribute("maxOccurs","unbounded"); if (cDef.getMinMultiplicity() == MinMult.ZERO) childEl.setAttribute("minOccurs","0"); seqEl.appendChild(childEl); String childTypeName = ""; // Expand the child element definition if necessary ElementDef childDef = cDef; if ((!cDef.isExpanded()) && (typeRepeats(cDef) < MAX_TYPE_REPEATS)) childDef = mappedStructure.getStructureDefinition().typeStructure(cDef.getType()); if (childDef == null) childDef = cDef; // revert if there is no type definition // if a child element has no child elements and no attributes, give it type 'xs:string' if ((childDef.getChildElements().size() == 0) && (childDef.getAttributeDefs().size() == 0)) childTypeName = XSDPrefix + ":string"; // allow an element with no child nodes to have a string value, and do not recurse else if (hasNoChildren(childDef)) childTypeName = XSDPrefix + ":string"; // otherwise, recursively make the type for the child element and all its descendants else childTypeName = makeType(outFile, outFile.topOut(),childDef, true); childEl.setAttribute("type",childTypeName); } if (children > 0) typeEl.appendChild(seqEl); // describe the attributes of the type for (Iterator<AttributeDef> ia = elDef.getAttributeDefs().iterator(); ia.hasNext();) { AttributeDef attDef = ia.next(); Element attEl = schemaElement(outFile,"attribute"); attEl.setAttribute("name", attDef.getName()); String type = XSDPrefix + ":string"; if (!attDef.getType().equals("")) type = attDef.getType(); attEl.setAttribute("type", type); if (attDef.getMinMultiplicity() == MinMult.ONE) attEl.setAttribute("use", "required"); typeEl.appendChild(attEl); // pick up any fixed value constraint from the description of the AttributeDef if (attDef.getDescription().startsWith(MakeITSMappingsAction.FIXED)) attEl.setAttribute("fixed", attDef.getDescription().substring(MakeITSMappingsAction.FIXED.length())); } } return typeName; } /** * @param el * @return node name to be used in the schema - either the name of the ElementDef, or a CDA Name given in its description */ private String nodeNameForSchema(ElementDef el) { String nodeName = el.getName(); String desc = el.getDescription(); if (desc.startsWith(MakeITSMappingsAction.CDA_NAME)) nodeName = desc.substring(MakeITSMappingsAction.CDA_NAME.length()); return nodeName; } /** * An element must be able to contain text if it has any property mappings * @param elDef * @return */ private boolean containsText(ElementDef elDef) { boolean hasText = false; if (elDef.getNodeMappingSet() != null) hasText = elDef.getNodeMappingSet().getPropertyMappings().size() > 0; return hasText; } /** * true if an element has no child nodes * @param elDef * @return */ private boolean hasNoChildren(ElementDef elDef) { return ((elDef.getChildElements().size() == 0) && (elDef.getAttributeDefs().size() == 0)); } /** * * @param name * @return a complex type name, based on the name, that has not been used before, * or is an exact match for one that has been used before */ private String newComplexTypeName(ElementDef currentElDef) { // the first choice name has not been used, or has been used and the type matches String name = currentElDef.getName(); String typeName = name + "_type"; ElementDef typeElDef = complexTypes.get(typeName); boolean alreadyUsed = (typeElDef != null); if ((alreadyUsed) && (congruent(currentElDef, typeElDef))) return typeName; if (!alreadyUsed) return typeName; // test a sequence of possible names, until you find one that is unused or matches int index = 1; while (alreadyUsed) { typeName = name + "_type_" + index; index++; typeElDef = complexTypes.get(typeName); alreadyUsed = (typeElDef != null); if ((alreadyUsed) && (congruent(currentElDef, typeElDef))) return typeName; if (!alreadyUsed) return typeName; } return typeName; // should be unreachable } /** * @param elDef1 * @param elDef2 * @return true if the two elements are congruent, i.e can be described by the same complex type */ private boolean congruent(ElementDef elDef1, ElementDef elDef2) { // they must have the same name, and the same number of attributes and child elements boolean congruent = ((elDef1.getName().equals(elDef2.getName())) && (elDef1.getAttributeDefs().size() == elDef2.getAttributeDefs().size()) && (elDef1.getChildElements().size() == elDef2.getChildElements().size())); if (!congruent) return false; // check the AttributeDefs match in name and min multiplicity and type and fixed values for (Iterator<AttributeDef> it = elDef1.getAttributeDefs().iterator(); it.hasNext();) { AttributeDef at1 = it.next(); AttributeDef at2 = elDef2.getNamedAttribute(at1.getName()); if (at2 == null) return false; if (at1.getMinMultiplicity().getValue() != at2.getMinMultiplicity().getValue()) return false; if (!at1.getType().equals(at2.getType())) return false; // types are not null // if either attribute has a fixed value, they must both have the same fixed value if ((at1.getDescription().startsWith(MakeITSMappingsAction.FIXED))|| (at2.getDescription().startsWith(MakeITSMappingsAction.FIXED))) { if (!at1.getDescription().equals(at2.getDescription())) return false; } } // check the child ElementDefs match in name and multiplicity and type for (Iterator<ElementDef> it = elDef1.getChildElements().iterator(); it.hasNext();) { ElementDef ed1 = it.next(); ElementDef ed2 = elDef2.getNamedChildElement(ed1.getName()); if (ed2 == null) return false; if (ed1.getMinMultiplicity().getValue() !=ed2.getMinMultiplicity().getValue()) return false; if (ed1.getMaxMultiplicity().getValue() !=ed2.getMaxMultiplicity().getValue()) return false; if (!ed1.getType().equals(ed2.getType())) return false; // types are not null // if the two elements already have a matching type, there is no need to check further if (!ed1.getType().equals("")) return true; // if they have no type, recurse further (because types are never expanded, this will not diverge) if (!congruent(ed1,ed2)) return false; } return true; } //------------------------------------------------------------------------------------------ // Open the schema to work out enumerated simple data types //------------------------------------------------------------------------------------------ private void resolveEnumeratedDataTypes() throws MapperException { XSDStructure schemaStructure = (XSDStructure)mappedStructure.getStructureDefinition(); Hashtable<String,Vector<String>> typeValues = new Hashtable<String,Vector<String>>(); /* iterate over the mapped structure, finding all simple types of AttributeDefs * or ElementDefs that have property mappings */ ElementDef root = mappedStructure.getRootElement(); resolveEnumeratedTypes(root,schemaStructure,typeValues); // save the values in attribute descriptions in the mapping set try {mappedStructure.eResource().save(null);} catch (Exception ex) {throw new MapperException(ex.getMessage());} } /** * * @param elDef * @param schemaStructure */ private void resolveEnumeratedTypes(ElementDef elDef,XSDStructure schemaStructure,Hashtable<String,Vector<String>> typeValues) { for (Iterator<AttributeDef> it = elDef.getAttributeDefs().iterator();it.hasNext();) { AttributeDef attDef = it.next(); String type = attDef.getType(); // get the values only once for each type if (typeValues.get(type)== null) { try { Vector<String> values = schemaStructure.getSimpleTypeEnumeratedValues(type); if (values.size() > 0) { // trace("Enumerated values for type " + type + ": " + GenUtil.singleString(values)); typeValues.put(type, values); } } catch (MapperException ex) { trace(ex.getMessage()); typeValues.put(type, new Vector<String>()); } } // mark up the AttributeDef in the mapping set, if it has any enumerated values Vector<String> values = typeValues.get(type); if ((values != null) && (values.size() > 0)) { Vector<String> quotedValues = new Vector<String>(); for (Iterator<String> ix = values.iterator();ix.hasNext();) quotedValues.add("'" + ix.next() + "'"); attDef.setDescription("Allowed values: " + GenUtil.singleString(quotedValues)); } } for (Iterator<ElementDef> it = elDef.getChildElements().iterator();it.hasNext();) { ElementDef child = it.next(); resolveEnumeratedTypes(child,schemaStructure,typeValues); } } //------------------------------------------------------------------------------------------ // Trivia //------------------------------------------------------------------------------------------ private void trace(String s) {if (tracing) System.out.println(s);} private void errorMessage(String s) { WorkBenchUtil.showMessage("Error",s); } }