package ca.uhn.fhir.tinder.parser; import static org.apache.commons.lang3.StringUtils.isBlank; import static org.apache.commons.lang3.StringUtils.isNotBlank; import java.io.InputStream; import java.io.InputStreamReader; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.text.WordUtils; import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.plugin.MojoFailureException; import org.hl7.fhir.instance.hapi.validation.DefaultProfileValidationSupport; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.NodeList; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.model.dstu2.resource.Bundle; import ca.uhn.fhir.model.dstu2.resource.Bundle.Entry; import ca.uhn.fhir.model.dstu2.resource.ValueSet; import ca.uhn.fhir.tinder.model.AnyChild; import ca.uhn.fhir.tinder.model.BaseElement; import ca.uhn.fhir.tinder.model.BaseRootType; import ca.uhn.fhir.tinder.model.Child; import ca.uhn.fhir.tinder.model.ResourceBlock; import ca.uhn.fhir.tinder.model.ResourceBlockCopy; import ca.uhn.fhir.tinder.model.SearchParameter; import ca.uhn.fhir.tinder.model.SimpleChild; import ca.uhn.fhir.tinder.util.XMLUtils; public abstract class BaseStructureSpreadsheetParser extends BaseStructureParser { public BaseStructureSpreadsheetParser(String theVersion, String theBaseDir) { super(theVersion, theBaseDir); myBindingStrengths = new HashMap<String, String>(); myBindingRefs = new HashMap<String, String>(); } private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(BaseStructureSpreadsheetParser.class); private int myColBinding = -1; private int myColModifier = -1; private int myColSummary = -1; private int myColCard = -1; private int myColDefinition = -1; private int myColName = -1; private int myColRequirements = -1; private int myColShortName = -1; private int myColType = -1; private int myColV2Mapping = -1; private HashMap<String, String> myBindingStrengths; private HashMap<String, String> myBindingRefs; private HashMap<String, String> myNameToValueSetUrl; public void parse() throws Exception { myNameToValueSetUrl = new HashMap<String, String>(); if (getVersion().equals("dstu2")) { ourLog.info("Loading ValueSets..."); FhirContext ctx = FhirContext.forDstu2(); String path = ctx.getVersion().getPathToSchemaDefinitions().replace("/schema", "/valueset") + "/valuesets.xml"; InputStream valuesetText = DefaultProfileValidationSupport.class.getResourceAsStream(path); Bundle bundle = ctx.newXmlParser().parseResource(Bundle.class, new InputStreamReader(valuesetText)); for (Entry next : bundle.getEntry()) { ValueSet nextVs = (ValueSet) next.getResource(); myNameToValueSetUrl.put(nextVs.getName(), nextVs.getUrl()); } ourLog.info("Done Loading ValueSets"); } int index = 0; for (InputStream nextInputStream : getInputStreams()) { String spreadsheetName = getInputStreamNames().get(index); ourLog.debug("Reading spreadsheet file {}", spreadsheetName); Document file; try { file = XMLUtils.parse(nextInputStream, false); } catch (Exception e) { throw new Exception("Failed during reading: " + spreadsheetName, e); } Element bindingsSheet = findSheetByName(spreadsheetName, "Bindings", file, false); if (bindingsSheet != null) { processBindingsSheet(bindingsSheet); } Element dataElementsSheet = findSheetByName(spreadsheetName, "Data Elements", file, true); NodeList tableList = dataElementsSheet.getElementsByTagName("Table"); Element table = (Element) tableList.item(0); NodeList rows = table.getElementsByTagName("Row"); Element defRow = (Element) rows.item(0); parseFirstRow(defRow); Element resourceRow = (Element) rows.item(1); BaseRootType resource = createRootType(); addResource(resource); parseBasicElements(resourceRow, resource, null); parseParameters(file, resource); resource.setId(resource.getName().toLowerCase()); if (this instanceof ResourceGeneratorUsingSpreadsheet) { resource.setProfile("http://hl7.org/fhir/profiles/" + resource.getElementName()); } Map<String, BaseElement> elements = new HashMap<String, BaseElement>(); elements.put(resource.getElementName(), resource); // Map<String,String> blockFullNameToShortName = new // HashMap<String,String>(); Map<String, List<String>> pathToResourceTypes = new HashMap<String, List<String>>(); List<Child> blockCopies = new ArrayList<Child>(); for (int i = 2; i < rows.getLength(); i++) { Element nextRow = (Element) rows.item(i); String name = cellValue(nextRow, 0); if (name == null || name.startsWith("!")) { continue; } String type = cellValue(nextRow, myColType); if (i < rows.getLength() - 1) { Element followingRow = (Element) rows.item(i + 1); if (followingRow != null) { String followingName = cellValue(followingRow, 0); if (followingName != null && followingName.startsWith(name + ".")) { type = ""; } } } Child elem; if (StringUtils.isBlank(type) || type.startsWith("=")) { elem = new ResourceBlock(); } else if (type.startsWith("@")) { // type = type.substring(type.lastIndexOf('.')+1); elem = new ResourceBlockCopy(); blockCopies.add(elem); } else if (type.equals("*")) { elem = new AnyChild(); } else { elem = new SimpleChild(); } parseBasicElements(nextRow, elem, type); postProcess(elem); elements.put(elem.getName(), elem); BaseElement parent = elements.get(elem.getElementParentName()); if (parent == null) { throw new Exception("Can't find element " + elem.getElementParentName() + " - Valid values are: " + elements.keySet()); } parent.addChild(elem); /* * Find simple setters */ if (elem instanceof Child) { scanForSimpleSetters(elem); } pathToResourceTypes.put(name, elem.getType()); } postProcess(resource); // for (SearchParameter nextParam : resource.getSearchParameters()) { // if (nextParam.getType().equals("reference")) { // String path = nextParam.getPath(); // List<String> targetTypes = pathToResourceTypes.get(path); // if (targetTypes != null) { // targetTypes = new ArrayList<String>(targetTypes); // for (Iterator<String> iter = targetTypes.iterator(); iter.hasNext();) { // String next = iter.next(); // if (next.equals("Any") || next.endsWith("Dt")) { // iter.remove(); // } // } // } // nextParam.setTargetTypes(targetTypes); // } // } // resolve BlockCopy elements so they can access // the children of the referenced ResourceBlock // from Velocity templates. for (Child blockCopy : blockCopies) { BaseElement element = blockCopy; refLoop: while (element.getElementParentName() != null) { BaseElement parent = elements.get(element.getElementParentName()); List<BaseElement> children = parent.getChildren(); for (BaseElement child : children) { if (!child.equals(blockCopy) && child instanceof ResourceBlock && child.getElementName().equals(blockCopy.getElementName())) { ((ResourceBlockCopy)blockCopy).setReferencedBlock((ResourceBlock)child); break refLoop; } } element = parent; } } index++; } ourLog.info("Parsed {} spreadsheet structures", getResources().size()); } private Element findSheetByName(String spreadsheetName, String wantedName, Document file, boolean theFailIfNotFound) throws Exception { Element retVal = null; for (int i = 0; i < file.getElementsByTagName("Worksheet").getLength() && retVal == null; i++) { retVal = (Element) file.getElementsByTagName("Worksheet").item(i); if (!wantedName.equals(retVal.getAttributeNS("urn:schemas-microsoft-com:office:spreadsheet", "Name"))) { retVal = null; } } if (retVal == null && theFailIfNotFound) { throw new Exception("Failed to find worksheet with name '" + wantedName + "' in spreadsheet: " + spreadsheetName); } return retVal; } private void processBindingsSheet(Element theBindingsSheet) { NodeList tableList = theBindingsSheet.getElementsByTagName("Table"); Element table = (Element) tableList.item(0); NodeList rows = table.getElementsByTagName("Row"); Element defRow = (Element) rows.item(0); int colName = 0; int colStrength = 0; int colRef = 0; for (int j = 0; j < 20; j++) { String nextName = cellValue(defRow, j); if (nextName == null) { continue; } nextName = nextName.toLowerCase().trim().replace(".", ""); if ("name".equals(nextName) || "binding name".equals(nextName)) { colName = j; } else if ("reference".equals(nextName)) { colRef = j; } else if ("conformance".equals(nextName)) { colStrength = j; } } for (int j = 1; j < rows.getLength(); j++) { Element nextRow = (Element) rows.item(j); String name = cellValue(nextRow, colName); String strength = cellValue(nextRow, colStrength); String ref = cellValue(nextRow, colRef); if (isNotBlank(name) && isNotBlank(strength)) { myBindingStrengths.put(name, strength); } if (isNotBlank(name) && isNotBlank(ref)) { if (ref.startsWith("#")) { ref = "http://hl7.org/fhir/ValueSet/" + ref.substring(1); } else if (!ref.startsWith("http")) { ref = "http://hl7.org/fhir/ValueSet/" + ref; } myBindingRefs.put(name, ref); } } } protected abstract BaseRootType createRootType(); private void parseParameters(Document theFile, BaseRootType theResource) throws MojoExecutionException { NodeList sheets = theFile.getElementsByTagName("Worksheet"); for (int i = 0; i < sheets.getLength(); i++) { Element sheet = (Element) sheets.item(i); String name = sheet.getAttributeNS("urn:schemas-microsoft-com:office:spreadsheet", "Name"); if ("Search".equals(name)) { NodeList tableList = sheet.getElementsByTagName("Table"); Element table = (Element) tableList.item(0); NodeList rows = table.getElementsByTagName("Row"); Element defRow = (Element) rows.item(0); int colName = 0; int colDesc = 0; int colType = 0; int colPath = 0; int colTargetTypes = 0; for (int j = 0; j < 20; j++) { String nextName = cellValue(defRow, j); if (nextName == null) { continue; } nextName = nextName.toLowerCase().trim().replace(".", ""); if ("name".equals(nextName)) { colName = j; } else if ("description".equals(nextName)) { colDesc = j; } else if ("type".equals(nextName)) { colType = j; } else if ("path".equals(nextName)) { colPath = j; } else if ("target types".equals(nextName)) { colTargetTypes = j; } } List<SearchParameter> compositeParams = new ArrayList<SearchParameter>(); for (int j = 1; j < rows.getLength(); j++) { Element nextRow = (Element) rows.item(j); SearchParameter sp = new SearchParameter(getVersion(), theResource.getName()); sp.setName(cellValue(nextRow, colName)); sp.setDescription(cellValue(nextRow, colDesc)); sp.setType(cellValue(nextRow, colType)); sp.setPath(cellValue(nextRow, colPath)); if (StringUtils.isNotBlank(sp.getName()) && !sp.getName().startsWith("!")) { if (sp.getType().equals("composite")) { compositeParams.add(sp); } else { theResource.addSearchParameter(sp); } } String targetTypes = cellValue(nextRow, colTargetTypes); if (isNotBlank(targetTypes)) { for (String next : targetTypes.trim().split("\\s*,\\s*")) { if (isNotBlank(next)) { if (Character.isUpperCase(next.charAt(0))) { sp.addTargetType(next); } } } } } for (SearchParameter nextCompositeParam : compositeParams) { // if(true)continue; if (isBlank(nextCompositeParam.getPath())) { throw new MojoExecutionException("Composite param " + nextCompositeParam.getName() + " has no path"); } if (nextCompositeParam.getPath().indexOf('&') == -1) { throw new MojoExecutionException("Composite param " + nextCompositeParam.getName() + " has path with no '&': " + nextCompositeParam.getPath()); } String[] parts = nextCompositeParam.getPath().split("\\&"); List<List<SearchParameter>> compositeOf = new ArrayList<List<SearchParameter>>(); for (String nextPart : parts) { nextPart = nextPart.trim(); if (isBlank(nextPart)) { continue; } List<SearchParameter> part = new ArrayList<SearchParameter>(); compositeOf.add(part); Set<String> possibleMatches = new HashSet<String>(); possibleMatches.add(nextPart); possibleMatches.add(theResource.getName() + "." + nextPart); possibleMatches.add(nextPart.replace("[x]", "-[x]")); possibleMatches.add(theResource.getName() + "." + nextPart.replace("[x]", "-[x]")); possibleMatches.add(nextPart.replace("-[x]", "[x]")); possibleMatches.add(theResource.getName() + "." + nextPart.replace("-[x]", "[x]")); for (SearchParameter nextParam : theResource.getSearchParameters()) { if (possibleMatches.contains(nextParam.getPath()) || possibleMatches.contains(nextParam.getName())) { part.add(nextParam); } } /* * Paths have changed in DSTU2 */ for (SearchParameter nextParam : theResource.getSearchParameters()) { if (nextPart.equals("value[x]") && (nextParam.getName().startsWith("value-"))) { part.add(nextParam); } if (nextPart.equals("component-value[x]") && (nextParam.getName().startsWith("component-value-"))) { part.add(nextParam); } } if (part.isEmpty()) { throw new MojoExecutionException("Composite param " + nextCompositeParam.getName() + " has path that doesn't seem to correspond to any other params: " + nextPart); } } if (compositeOf.size() > 2) { // TODO: change back to exception maybe? Grahame says these aren't allowed.. ourLog.warn("Composite param " + nextCompositeParam.getName() + " has >2 parts, this isn't supported yet"); continue; } for (SearchParameter part1 : compositeOf.get(0)) { for (SearchParameter part2 : compositeOf.get(1)) { SearchParameter composite = new SearchParameter(getVersion(), theResource.getName()); theResource.addSearchParameter(composite); composite.setName(part1.getName() + "-" + part2.getName()); composite.setDescription(nextCompositeParam.getDescription()); composite.setPath(nextCompositeParam.getPath()); composite.setType("composite"); composite.setCompositeOf(Arrays.asList(part1.getName(), part2.getName())); composite.setCompositeTypes(Arrays.asList(WordUtils.capitalize(part1.getType()), WordUtils.capitalize(part2.getType()))); } } } } } } protected abstract Collection<InputStream> getInputStreams(); protected abstract List<String> getInputStreamNames(); private void parseFirstRow(Element theDefRow) { for (int i = 0; i < 20; i++) { String nextName = cellValue(theDefRow, i); if (nextName == null) { continue; } nextName = nextName.toLowerCase().trim().replace(".", "").replace(" ", ""); if ("element".equals(nextName)) { myColName = i; } else if ("ismodifier".equals(nextName)) { myColModifier = i; } else if ("summary".equals(nextName)) { myColSummary = i; } else if ("card".equals(nextName)) { myColCard = i; } else if ("type".equals(nextName)) { myColType = i; } else if ("binding".equals(nextName)) { myColBinding = i; } else if ("shortname".equals(nextName)) { myColShortName = i; } else if ("definition".equals(nextName)) { myColDefinition = i; } else if ("requirements".equals(nextName)) { myColRequirements = i; } else if ("v2mapping".equals(nextName)) { myColV2Mapping = i; } } if (myColName == -1) { throw new IllegalArgumentException("Unable to determine column: name"); } if (myColModifier == -1) { throw new IllegalArgumentException("Unable to determine column: modifier"); } if (myColCard == -1) { throw new IllegalArgumentException("Unable to determine column: card"); } if (myColType == -1) { throw new IllegalArgumentException("Unable to determine column: type"); } if (myColBinding == -1) { throw new IllegalArgumentException("Unable to determine column: binding"); } if (myColDefinition == -1) { throw new IllegalArgumentException("Unable to determine column: definition"); } if (myColRequirements == -1) { throw new IllegalArgumentException("Unable to determine column: requirements"); } if (myColV2Mapping == -1) { throw new IllegalArgumentException("Unable to determine column: v2 mapping"); } } private void parseBasicElements(Element theRowXml, BaseElement theTarget, String theTypeOrNull) { String name = cellValue(theRowXml, myColName); theTarget.setName(name); theTarget.setElementNameAndDeriveParentElementName(name); String cardValue = cellValue(theRowXml, myColCard); if (cardValue != null && cardValue.contains("..")) { String[] split = cardValue.split("\\.\\."); theTarget.setCardMin(split[0]); theTarget.setCardMax(split[1]); } String type = theTypeOrNull != null ? theTypeOrNull : cellValue(theRowXml, myColType); theTarget.setTypeFromString(type); theTarget.setBinding(cellValue(theRowXml, myColBinding)); theTarget.setShortName(cellValue(theRowXml, myColShortName)); theTarget.setDefinition(cellValue(theRowXml, myColDefinition)); theTarget.setRequirement(cellValue(theRowXml, myColRequirements)); theTarget.setV2Mapping(cellValue(theRowXml, myColV2Mapping)); theTarget.setSummary(cellValue(theRowXml, myColSummary)); theTarget.setModifier(cellValue(theRowXml, myColModifier)); // Per #320 if ("example".equals(myBindingStrengths.get(theTarget.getBinding()))) { theTarget.setBinding(null); } if (isNotBlank(theTarget.getBinding())) { String bindingUrl = myBindingRefs.get(theTarget.getBinding()); if (isNotBlank(bindingUrl)) { theTarget.setBindingUrl(bindingUrl); } else { bindingUrl = myNameToValueSetUrl.get(theTarget.getBinding()); if (isNotBlank(bindingUrl)) { theTarget.setBindingUrl(bindingUrl); } } } } /** * Subclasses may override */ protected void postProcess(BaseElement theTarget) throws MojoFailureException { // nothing } }