diff --git a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/conformance/ShExGenerator.java b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/conformance/ShExGenerator.java index d119aaa97..924b3733c 100644 --- a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/conformance/ShExGenerator.java +++ b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/conformance/ShExGenerator.java @@ -31,27 +31,16 @@ package org.hl7.fhir.r5.conformance; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.Comparator; -import java.util.HashSet; -import java.util.LinkedList; -import java.util.List; +import java.util.*; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.tuple.ImmutablePair; import org.apache.commons.lang3.tuple.Pair; import org.hl7.fhir.r5.conformance.profile.ProfileUtilities; import org.hl7.fhir.r5.context.IWorkerContext; -import org.hl7.fhir.r5.model.Constants; -import org.hl7.fhir.r5.model.DataType; -import org.hl7.fhir.r5.model.DomainResource; -import org.hl7.fhir.r5.model.ElementDefinition; -import org.hl7.fhir.r5.model.Enumerations; -import org.hl7.fhir.r5.model.StructureDefinition; -import org.hl7.fhir.r5.model.ValueSet; +import org.hl7.fhir.r5.model.*; import org.hl7.fhir.r5.terminologies.ValueSetExpander; +import org.hl7.fhir.r5.utils.FHIRPathEngine; import org.stringtemplate.v4.ST; public class ShExGenerator { @@ -59,22 +48,34 @@ public class ShExGenerator { public enum HTMLLinkPolicy { NONE, EXTERNAL, INTERNAL } + + public enum ConstraintTranslationPolicy { + ALL, // Translate all Extensions found; Default (or when no policy defined) + GENERIC_ONLY, // Translate all Extensions except constraints with context-of-use + CONTEXT_OF_USE_ONLY // Translate only Extensions with context-of-use + } + public boolean doDatatypes = true; // add data types public boolean withComments = true; // include comments public boolean completeModel = false; // doing complete build (fhir.shex) + public boolean debugMode = false; + + public ConstraintTranslationPolicy constraintPolicy = ConstraintTranslationPolicy.ALL; private static String SHEX_TEMPLATE = "$header$\n\n" + - "$shapeDefinitions$"; + + "$shapeDefinitions$"; // A header is a list of prefixes, a base declaration and a start node private static String FHIR = "http://hl7.org/fhir/"; private static String FHIR_VS = FHIR + "ValueSet/"; private static String HEADER_TEMPLATE = - "PREFIX fhir: <$fhir$> \n" + - "PREFIX fhirvs: <$fhirvs$>\n" + - "PREFIX xsd: \n" + - "BASE \n$start$"; + "PREFIX fhir: <$fhir$> \n" + + "PREFIX fhirvs: <$fhirvs$>\n" + + "PREFIX xsd: \n" + + "PREFIX rdf: \n" + + "BASE \n$start$"; // Start template for single (open) entry private static String START_TEMPLATE = "\n\nstart=@<$id$> AND {fhir:nodeRole [fhir:treeRoot]}\n"; @@ -93,23 +94,50 @@ public class ShExGenerator { // the list of element declarations // an optional index element (for appearances inside ordered lists) private static String SHAPE_DEFINITION_TEMPLATE = - "$comment$\n<$id$> CLOSED {\n $resourceDecl$" + - "\n $elements$" + - "\n fhir:index xsd:integer? # Relative position in a list\n}\n"; + "$comment$\n<$id$> CLOSED { $fhirType$ " + + "\n $resourceDecl$" + + "\n $elements$" + + "\n $contextOfUse$" + + "\n} $constraints$ \n"; + + // Base DataTypes + private List baseDataTypes = Arrays.asList( + "DataType", + "PrimitiveType" + ); + + private List mappedFunctions = Arrays.asList( + "empty", + "exists", + "hasValue", + "matches", + "contains", + "toString", + "is", + "where" + ); + + private static String ONE_OR_MORE_PREFIX = "OneOrMore_"; + private static String ONE_OR_MORE_CHOICES = "_One-Or-More-Choices_"; + private static String ONE_OR_MORE_TEMPLATE = + "\n$comment$\n<$oomType$> CLOSED {" + + "\n rdf:first @<$origType$> $restriction$ ;" + + "\n rdf:rest [rdf:nil] OR @<$oomType$> " + + "\n}\n"; // Resource Definition // an open shape of type Resource. Used when completeModel = false. private static String RESOURCE_SHAPE_TEMPLATE = - "$comment$\n {a .+;" + - "\n $elements$" + - "\n fhir:index xsd:integer?" + - "\n}\n"; + "$comment$\n {a .+;" + + "\n $elements$" + + "\n $contextOfUse$" + + "\n} $constraints$ \n"; // If we have knowledge of all of the possible resources available to us (completeModel = true), we can build // a model of all possible resources. private static String COMPLETE_RESOURCE_TEMPLATE = - " @<$resources$>" + - "\n\n"; + " @<$resources$>" + + "\n\n"; // Resource Declaration // a type node @@ -176,18 +204,18 @@ public class ShExGenerator { // A typed reference -- a fhir:uri with an optional type and the possibility of a resolvable shape private static String TYPED_REFERENCE_TEMPLATE = "\n<$refType$Reference> CLOSED {" + - "\n fhir:Element.id @?;" + - "\n fhir:Element.extension @*;" + - "\n fhir:link @<$refType$> OR CLOSED {a [fhir:$refType$]}?;" + - "\n fhir:Reference.reference @?;" + - "\n fhir:Reference.display @?;" + - "\n fhir:index xsd:integer?" + - "\n}"; + "\n fhir:Element.id @?;" + + "\n fhir:Element.extension @*;" + + "\n fhir:link @<$refType$> OR CLOSED {a [fhir:$refType$]}?;" + + "\n fhir:Reference.reference @?;" + + "\n fhir:Reference.display @?;" + + // "\n fhir:index xsd:integer?" + + "\n}"; private static String TARGET_REFERENCE_TEMPLATE = "\n<$refType$> {" + - "\n a [fhir:$refType$];" + - "\n fhir:nodeRole [fhir:treeRoot]?" + - "\n}"; + "\n a [fhir:$refType$];" + + "\n fhir:nodeRole [fhir:treeRoot]?" + + "\n}"; // A value set definition private static String VALUE_SET_DEFINITION = "# $comment$\n$vsuri$$val_list$\n"; @@ -208,6 +236,13 @@ public class ShExGenerator { * doDataTypes -- whether or not to emit the data types. */ private HashSet> innerTypes, emittedInnerTypes; + + private List oneOrMoreTypes; + + private List constraintsList; + + private List unMappedFunctions; + private HashSet datatypes, emittedDatatypes; private HashSet references; private LinkedList uniq_structures; @@ -215,23 +250,43 @@ public class ShExGenerator { private HashSet required_value_sets; private HashSet known_resources; // Used when generating a full definition + // List of URLs of Excluded Structure Definitions from ShEx Schema generation. + private List excludedSDUrls; + + // List of URLs of selected Structure Definitions of Extensions from ShEx Schema generation. + // Extensions are Structure Definitions with type as "Extension". + private List selectedExtensions; + private List selectedExtensionUrls; + private FHIRPathEngine fpe; + public ShExGenerator(IWorkerContext context) { super(); this.context = context; profileUtilities = new ProfileUtilities(context, null, null); innerTypes = new HashSet>(); + oneOrMoreTypes = new ArrayList(); + constraintsList = new ArrayList(); + unMappedFunctions = new ArrayList(); emittedInnerTypes = new HashSet>(); datatypes = new HashSet(); emittedDatatypes = new HashSet(); references = new HashSet(); required_value_sets = new HashSet(); known_resources = new HashSet(); + excludedSDUrls = new ArrayList(); + selectedExtensions = new ArrayList(); + selectedExtensionUrls = new ArrayList(); + + fpe = new FHIRPathEngine(context); } public String generate(HTMLLinkPolicy links, StructureDefinition structure) { List list = new ArrayList(); list.add(structure); innerTypes.clear(); + oneOrMoreTypes.clear(); + constraintsList.clear(); + unMappedFunctions.clear(); emittedInnerTypes.clear(); datatypes.clear(); emittedDatatypes.clear(); @@ -241,6 +296,29 @@ public class ShExGenerator { return generate(links, list); } + public List getExcludedStructureDefinitionUrls(){ + return this.excludedSDUrls; + } + + public void setExcludedStructureDefinitionUrls(List excludedSDs){ + this.excludedSDUrls = excludedSDs; + } + + public List getSelectedExtensions(){ + return this.selectedExtensions; + } + + public void setSelectedExtension(List selectedExtensions){ + this.selectedExtensions = selectedExtensions; + + selectedExtensionUrls.clear(); + + for (StructureDefinition eSD : selectedExtensions){ + if (!selectedExtensionUrls.contains(eSD.getUrl())) + selectedExtensionUrls.add(eSD.getUrl()); + } + } + public class SortById implements Comparator { @Override @@ -262,50 +340,116 @@ public class ShExGenerator { * @param structures list of structure definitions to render * @return ShEx definition of structures */ - public String generate(HTMLLinkPolicy links, List structures) { + public String generate(HTMLLinkPolicy links, List structures, List excludedSDUrls) { + this.excludedSDUrls = excludedSDUrls; + if ((structures != null )&&(this.selectedExtensions != null)){ + structures.addAll(this.selectedExtensions); + } + + return generate(links, structures); + } + + /** + * this is called externally to generate a set of structures to a single ShEx file + * generally, it will be called with a single structure, or a long list of structures (all of them) + * + * @param links HTML link rendering policy + * @param structures list of structure definitions to render + * @return ShEx definition of structures + */ + public String generate(HTMLLinkPolicy links, List structures) { ST shex_def = tmplt(SHEX_TEMPLATE); String start_cmd; if(completeModel || structures.get(0).getKind().equals(StructureDefinition.StructureDefinitionKind.RESOURCE)) -// || structures.get(0).getKind().equals(StructureDefinition.StructureDefinitionKind.COMPLEXTYPE)) start_cmd = completeModel? tmplt(ALL_START_TEMPLATE).render() : - tmplt(START_TEMPLATE).add("id", structures.get(0).getId()).render(); + tmplt(START_TEMPLATE).add("id", structures.get(0).getId()).render(); else start_cmd = ""; - shex_def.add("header", tmplt(HEADER_TEMPLATE). - add("start", start_cmd). - add("fhir", FHIR). - add("fhirvs", FHIR_VS).render()); + + shex_def.add("header", + tmplt(HEADER_TEMPLATE). + add("start", start_cmd). + add("fhir", FHIR). + add("fhirvs", FHIR_VS).render()); Collections.sort(structures, new SortById()); StringBuilder shapeDefinitions = new StringBuilder(); - // For unknown reasons, the list of structures carries duplicates. We remove them - // Also, it is possible for the same sd to have multiple hashes... + // For unknown reasons, the list of structures carries duplicates. + // We remove them. Also, it is possible for the same sd to have multiple hashes... uniq_structures = new LinkedList(); uniq_structure_urls = new HashSet(); for (StructureDefinition sd : structures) { + // Exclusion Criteria... + if ((excludedSDUrls != null) && + (excludedSDUrls.contains(sd.getUrl()))) { + printBuildMessage("SKIPPED Generating ShEx for " + sd.getName() + " [ " + sd.getUrl() + " ] !"); + printBuildMessage("Reason: It is in excluded list of structures."); + continue; + } + + if ("Extension".equals(sd.getType())) { + if ((!this.selectedExtensionUrls.isEmpty()) && (!this.selectedExtensionUrls.contains(sd.getUrl()))) { + printBuildMessage("SKIPPED Generating ShEx for " + sd.getName() + " [ " + sd.getUrl() + " ] !"); + printBuildMessage("Reason: It is NOT included in the list of selected extensions."); + continue; + } + + if ((this.constraintPolicy == ConstraintTranslationPolicy.GENERIC_ONLY) && (sd.hasContext())) { + printBuildMessage("SKIPPED Generating ShEx for " + sd.getName() + " [ " + sd.getUrl() + " ] !"); + printBuildMessage("Reason: ConstraintTranslationPolicy is set to GENERIC_ONLY, and this Structure has Context of Use."); + continue; + } + + if ((this.constraintPolicy == ConstraintTranslationPolicy.CONTEXT_OF_USE_ONLY) && (!sd.hasContext())) { + printBuildMessage("SKIPPED Generating ShEx for " + sd.getName() + " [ " + sd.getUrl() + " ] !"); + printBuildMessage("Reason: ConstraintTranslationPolicy is set to CONTEXT_OF_USE_ONLY, and this Structure has no Context of Use."); + continue; + } + } + if (!uniq_structure_urls.contains(sd.getUrl())) { uniq_structures.add(sd); uniq_structure_urls.add(sd.getUrl()); } } - + boolean isShapeDefinitionEmpty = true; for (StructureDefinition sd : uniq_structures) { - shapeDefinitions.append(genShapeDefinition(sd, true)); + printBuildMessage(" ---- Generating ShEx for : " + sd.getName() + " [ " + sd.getUrl() + " ] ..."); + String shapeDefinitionStr = genShapeDefinition(sd, true); + + if (!shapeDefinitionStr.isEmpty()) { + isShapeDefinitionEmpty = false; + shapeDefinitions.append(shapeDefinitionStr); + } + else { + printBuildMessage(" ---- EMPTY/No ShEx SCHEMA generated for : " + sd.getName() + " [ " + sd.getUrl() + " ]."); + } + } + + // There was not shape generated. return empty. + // No need to generate data types, references and valuesets + if (isShapeDefinitionEmpty) { + return ""; } shapeDefinitions.append(emitInnerTypes()); if(doDatatypes) { shapeDefinitions.append("\n#---------------------- Data Types -------------------\n"); while (emittedDatatypes.size() < datatypes.size() || - emittedInnerTypes.size() < innerTypes.size()) { + emittedInnerTypes.size() < innerTypes.size()) { shapeDefinitions.append(emitDataTypes()); shapeDefinitions.append(emitInnerTypes()); } } + shapeDefinitions.append("\n#---------------------- Cardinality Types (OneOrMore) -------------------\n"); + oneOrMoreTypes.forEach((String oomType) -> { + shapeDefinitions.append(getOneOrMoreType(oomType)); + }); + shapeDefinitions.append("\n#---------------------- Reference Types -------------------\n"); for(String r: references) { shapeDefinitions.append("\n").append(tmplt(TYPED_REFERENCE_TEMPLATE).add("refType", r).render()).append("\n"); @@ -316,20 +460,54 @@ public class ShExGenerator { if(completeModel && known_resources.size() > 0) { shapeDefinitions.append("\n").append(tmplt(COMPLETE_RESOURCE_TEMPLATE) - .add("resources", StringUtils.join(known_resources, "> OR\n\t@<")).render()); + .add("resources", StringUtils.join(known_resources, "> OR\n\t@<")).render()); List all_entries = new ArrayList(); for(String kr: known_resources) all_entries.add(tmplt(ALL_ENTRY_TEMPLATE).add("id", kr).render()); shapeDefinitions.append("\n").append(tmplt(ALL_TEMPLATE) - .add("all_entries", StringUtils.join(all_entries, " OR\n\t")).render()); + .add("all_entries", StringUtils.join(all_entries, " OR\n\t")).render()); } shapeDefinitions.append("\n#---------------------- Value Sets ------------------------\n"); for(ValueSet vs: required_value_sets) shapeDefinitions.append("\n").append(genValueSet(vs)); + + if ((unMappedFunctions != null)&&(!unMappedFunctions.isEmpty())) { + debug("------------------------- Unmapped Functions ---------------------"); + for (String um : unMappedFunctions) { + debug(um); + } + } + return shex_def.render(); } + private String getExtendedType(StructureDefinition sd){ + if (sd == null) + return null; + + String sId = sd.getId(); + String bd = ""; + if (sd.hasBaseDefinition()) { + bd = sd.getBaseDefinition(); + String[] els = bd.split("/"); + bd = els[els.length - 1]; + + sId += "> EXTENDS @<" + bd; + } + + return sId; + } + + private String getExtendedType(ElementDefinition ed){ + if (ed == null) + return ""; + String bd = (ed.getType().size() > 0)? (ed.getType().get(0).getCode()) : ""; + if (bd != null && !bd.isEmpty() && !baseDataTypes.contains(bd)) { + bd = "> EXTENDS @<" + bd; + } + return bd; + } /** * Emit a ShEx definition for the supplied StructureDefinition @@ -344,22 +522,21 @@ public class ShExGenerator { ST shape_defn; // Resources are either incomplete items or consist of everything that is defined as a resource (completeModel) + // if (sd.getName().equals("ActivityDefinition")){ + // debug("ActivityDefinition found"); + // } if("Resource".equals(sd.getName())) { shape_defn = tmplt(RESOURCE_SHAPE_TEMPLATE); known_resources.add(sd.getName()); } else { - shape_defn = tmplt(SHAPE_DEFINITION_TEMPLATE).add("id", sd.getId()); - if (sd.getKind().equals(StructureDefinition.StructureDefinitionKind.RESOURCE)) { -// || sd.getKind().equals(StructureDefinition.StructureDefinitionKind.COMPLEXTYPE)) { - known_resources.add(sd.getName()); - ST resource_decl = tmplt(RESOURCE_DECL_TEMPLATE). - add("id", sd.getId()). - add("root", tmplt(ROOT_TEMPLATE)); -// add("root", top_level ? tmplt(ROOT_TEMPLATE) : ""); - shape_defn.add("resourceDecl", resource_decl.render()); - } else { - shape_defn.add("resourceDecl", ""); - } + shape_defn = tmplt(SHAPE_DEFINITION_TEMPLATE).add("id", getExtendedType(sd)); + known_resources.add(sd.getName()); + ST resource_decl = tmplt(RESOURCE_DECL_TEMPLATE). + add("id", sd.getId()). + add("root", tmplt(ROOT_TEMPLATE)); + shape_defn.add("resourceDecl", resource_decl.render()); + + shape_defn.add("fhirType", " "); } // Generate the defining elements @@ -373,22 +550,465 @@ public class ShExGenerator { elements.add(tmplt(CONCEPT_REFERENCES_TEMPLATE).render()); else if (sdn.equals("Reference")) elements.add(tmplt(RESOURCE_LINK_TEMPLATE).render()); -// else if (sdn.equals("Extension")) -// return tmplt(EXTENSION_TEMPLATE).render(); String root_comment = null; + + constraintsList.clear(); for (ElementDefinition ed : sd.getSnapshot().getElement()) { if(!ed.getPath().contains(".")) root_comment = ed.getShort(); - else if (StringUtils.countMatches(ed.getPath(), ".") == 1 && !"0".equals(ed.getMax())) { - elements.add(genElementDefinition(sd, ed)); + else if ( + (StringUtils.countMatches(ed.getPath(), ".") == 1 && !"0".equals(ed.getMax())) + && (ed.hasBase() + && ( + ed.getBase().getPath().startsWith(sdn) + || (ed.getBase().getPath().startsWith("Extension")) + || (ed.getBase().getPath().startsWith("Element.extension")&&(ed.hasSliceName())) + ) + ) + ){ + String elementDefinition = genElementDefinition(sd, ed); + + boolean isInnerType = false; + if (isInInnerTypes(ed)){ + //debug("This element is already in innerTypes:" + ed.getPath()); + isInnerType = true; + } + + // Process constraints + for (ElementDefinition.ElementDefinitionConstraintComponent constraint : ed.getConstraint()) { + String sdType = sd.getType(); + String cstype = constraint.getSource(); + if ((!cstype.isEmpty()) && (cstype.indexOf("/") != -1)) { + String[] els = cstype.split("/"); + cstype = els[els.length - 1]; + } + // Implement here if SD type == constraint source OR SD type is Primitive or DataType then add constraint to SD otherwise skip it. + //if ((sdType.equals(cstype)) || baseDataTypes.contains(sdType) || baseDataTypes.contains(bd)) { + //if ((sdType.equals(cstype)) || baseDataTypes.contains(sdType)) { + String id = ed.hasBase() ? ed.getBase().getPath() : ed.getPath(); + String shortId = id.substring(id.lastIndexOf(".") + 1); + if ((ed.hasContentReference() && (!ed.hasType())) || (id.equals(sd.getName() + "." + shortId))) { + if ((sdType.equals(cstype)) || baseDataTypes.contains(sdType)) { + if (!isInnerType) { + debug("\n Key: " + constraint.getKey() + " SD type: " + sd.getType() + " Element: " + ed.getPath() + " Constraint Source: " + constraint.getSource() + " Constraint:" + constraint.getExpression()); + String transl = translateConstraint(sd, ed, constraint); + if (transl.isEmpty() || constraintsList.contains(transl)) + continue; + constraintsList.add(transl); + } + } + } + } + elements.add(elementDefinition); + } + else { + List children = profileUtilities.getChildList(sd, ed); + if (children.size() > 0) { + for (ElementDefinition child : children) { + if (child.getPath().startsWith(ed.getPath())) + innerTypes.add(new ImmutablePair(sd, ed)); + } + } + } + } + + // Constraints for differential to cover constraints on SD itself without any elements of its own + for (ElementDefinition ded : sd.getDifferential().getElement()) { + // Process constraints + for (ElementDefinition.ElementDefinitionConstraintComponent dconstraint : ded.getConstraint()) { + String sdType = sd.getType(); + + String id = ded.hasBase() ? ded.getBase().getPath() : ded.getPath(); + String shortId = id.substring(id.lastIndexOf(".") + 1); + + if (!isInInnerTypes(ded)) { + debug("\n Key: " + dconstraint.getKey() + " SD type: " + sd.getType() + " Element: " + ded.getPath() + " Constraint Source: " + dconstraint.getSource() + " Constraint:" + dconstraint.getExpression()); + String dtransl = translateConstraint(sd, ded, dconstraint); + if (dtransl.isEmpty() || constraintsList.contains(dtransl)) + continue; + constraintsList.add(dtransl); + } } } shape_defn.add("elements", StringUtils.join(elements, "\n")); shape_defn.add("comment", root_comment == null? " " : "# " + root_comment); + + String constraintStr = ""; + + if (!constraintsList.isEmpty()) { + constraintStr = "AND (\n\n" + StringUtils.join(constraintsList, "\n\n) AND (\n\n") + "\n\n)\n"; + } + + shape_defn.add("constraints", constraintStr); + + String contextOfUseStr = ""; + ArrayList contextOfUse = new ArrayList(); + if (!sd.getContext().isEmpty()) { + for (StructureDefinition.StructureDefinitionContextComponent uc : sd.getContext()) { + if (!uc.getExpression().isEmpty()) { + String toStore = uc.getExpression() ; + String[] backRefs = toStore.split("\\."); + toStore = "a [fhir:" + backRefs[0] + "]"; + for (int i = 1; i < backRefs.length; i++) + toStore = "^fhir:" + backRefs[i] + " {" + toStore + "}"; + + if (!contextOfUse.contains(toStore)) { + contextOfUse.add(toStore); + } + } + } + contextOfUseStr = "^fhir:extension { " + StringUtils.join(contextOfUse, " OR \n\t\t\t\t") + "\n\t\t}"; + } + + shape_defn.add("contextOfUse", contextOfUseStr); + return shape_defn.render(); } + /** + * @param ed + * @param constraint + * @return + */ + private String translateConstraint(StructureDefinition sd, ElementDefinition ed, ElementDefinition.ElementDefinitionConstraintComponent constraint){ + String translated = ""; + + if (constraint != null) { + String ce = constraint.getExpression(); + String constItem = "FHIR-SD-Path:" + ed.getPath() + " Expression: " + ce; + try { + translated = "# Constraint: UniqueKey:" + constraint.getKey() + "\n# Human readable:" + constraint.getHuman() + "\n# Constraint:" + constraint.getExpression() + "\n# ShEx:\n"; + + ExpressionNode expr = fpe.parse(ce); + String shexConstraint = processExpressionNode(sd, ed, expr, false, 0); + shexConstraint = shexConstraint.replaceAll("CALLER", ""); + debug(" Parsed to ShEx Constraint:" + shexConstraint); + if (!shexConstraint.isEmpty()) + translated += "\n" + shexConstraint; + + debug(" TRANSLATED\t"+ed.getPath()+"\t"+constraint.getHuman()+"\t"+constraint.getExpression()+"\t"+shexConstraint); + + } catch (Exception e) { + String message = " FAILED to parse the constraint: " + constItem + " [ " + e.getMessage() + " ]"; + // Now make this a comment so that it does not fail when schema is resolved in validator + // TODO: This needs to be fixed + // TODO: it should be + // translated = message + + translated = ""; + debug(message); + } + } + return translated; + } + + /** + * @param node + * @param quote + * @return + */ + private String processExpressionNode(StructureDefinition sd, ElementDefinition ed, ExpressionNode node, boolean quote, int depth) { + if (node == null) + return ""; + boolean toQuote = quote; + + String innerShEx = processExpressionNode(sd, ed, node.getInner(), quote, depth + 1); + + String translatedShEx = ""; + + boolean treatBothOpsSame = false; + // Figure out if there are any operations defined on this node + String ops = ""; + String endOps = ""; + if (node.getOperation() != null) { + String opName = node.getOperation().name(); + switch (opName) { + case "Or": + ops = " OR "; + break; + case "Union": + ops = " | "; + break; + case "In" : + case "Equals": + case "Contains": + ops = " { fhir:v ["; + endOps = "] } "; + toQuote = true; + break; + case "NotEquals": + ops = " [fhir:v . -"; + endOps = "] "; + toQuote = true; + break; + case "Greater": + if (node.getOpNext().getKind().equals(ExpressionNode.Kind.Constant)) { + ops = " { fhir:v MinExclusive "; + endOps = " } "; + } else { + String toStore = "UNMAPPED_OPERATOR_" + opName + " in Node type: " + node.getKind(); + addUnmappedFunction(opName); + ops = TBD(opName); + } + break; + case "GreaterOrEqual": + if (node.getOpNext().getKind().equals(ExpressionNode.Kind.Constant)) { + ops = " { fhir:v MinInclusive "; + endOps = " } "; + } else { + String toStore = "UNMAPPED_OPERATOR_" + opName + " in Node type: " + node.getKind(); + addUnmappedFunction(opName); + ops = TBD(opName); + } + break; + case "Less": + case "LessThan": + if (node.getOpNext().getKind().equals(ExpressionNode.Kind.Constant)) { + ops = " { fhir:v MaxExclusive "; + endOps = " } "; + } else { + String toStore = "UNMAPPED_OPERATOR_" + opName + " in Node type: " + node.getKind(); + addUnmappedFunction(opName); + ops = TBD(opName); + } + break; + case "LessOrEqual": + if (node.getOpNext().getKind().equals(ExpressionNode.Kind.Constant)) { + ops = " { fhir:v MaxInclusive "; + endOps = " } "; + } else { + String toStore = "UNMAPPED_OPERATOR_" + opName + " in Node type: " + node.getKind(); + addUnmappedFunction(opName); + ops = TBD(opName); + } + break; + case "And": + case "Implies" : + ops = " AND "; + break; + case "As": + case "Is": + ops = " a "; + break; + case "Xor": + ops = " XOR "; + break; + default: + String toStore = "UNMAPPED_OPERATOR_" + opName + " in Node type: " + node.getKind(); + if (!unMappedFunctions.contains(toStore)) + unMappedFunctions.add(toStore); + + ops = TBD(opName); + } + } + + // Functions + String fExp = ""; + String pExp = ""; + boolean isFunctionCall = false; + ExpressionNode.Kind kind = node.getKind(); + if (kind == ExpressionNode.Kind.Function) { + String funcName = node.getName(); + if (!mappedFunctions.contains(funcName)) { + if (node.parameterCount() == 0) { + if ("not".equals(funcName)) + fExp = " NOT { CALLER }"; + else { + fExp = " " + funcName + "( CALLER )"; + + addUnmappedFunction(node.getFunction().toCode()); + String toStore = addUnmappedFunction(node.getFunction().toCode()); + } + } + } + + if ("".equals(fExp)) { + switch (funcName) { + case "empty": + //fExp = " .?"; + fExp = " NOT { CALLER {fhir:v .} } " ; + break; + case "exists": + case "hasValue": + fExp = " ."; + break; + case "matches": + ops = " { fhir:v /"; + endOps = "/ } "; + break; + case "where": // 'where' just states an assertion + ops = "{ "; + endOps = " }"; + break; + case "contains": + ops = " { fhir:v ["; + endOps = "] } "; + toQuote = true; + break; + case "toString": // skip this function call because values gets stringitize anyway + pExp = ""; + break; + case "is": + ops = " { a ["; + endOps = "] } "; + break; + default: + fExp = TBD(node.getFunction().toCode()); + String toStore = addUnmappedFunction(node.getFunction().toCode()); + } + + if (node.parameterCount() > 0) { + for (ExpressionNode pen : node.getParameters()) { + if (!"".equals(pExp)) + pExp += ", "; + pExp += processExpressionNode(sd, ed, pen, quote, depth); + isFunctionCall = true; + } + } + } + + if (isFunctionCall) { + if (!mappedFunctions.contains(funcName)) + translatedShEx += fExp + "(" + pExp + ")"; + else { + translatedShEx += ops + pExp + endOps; + ops = ""; + endOps = ""; + } + } + else + translatedShEx += fExp; + + translatedShEx = positionParts(innerShEx, translatedShEx, + getNextOps(ops , processExpressionNode(sd, ed, node.getOpNext(), toQuote, depth), endOps, treatBothOpsSame), + depth, false); + + } else if (kind == ExpressionNode.Kind.Name) { + translatedShEx += positionParts(innerShEx, "fhir:" + node.getName(), + getNextOps(ops, processExpressionNode(sd, ed, node.getOpNext(), toQuote, depth), endOps, treatBothOpsSame), + depth, true); + }else if (kind == ExpressionNode.Kind.Group) { + translatedShEx += positionParts(innerShEx, processExpressionNode(sd, ed, node.getGroup(), toQuote, depth), + getNextOps(ops , processExpressionNode(sd, ed, node.getOpNext(), toQuote, depth), endOps, treatBothOpsSame), + depth, true); + } else if (kind == ExpressionNode.Kind.Constant) { + Base constantB = node.getConstant(); + boolean toQt = (constantB instanceof StringType) || (!constantB.isPrimitive()); + String constantV = constantB.primitiveValue(); + + if (constantV.startsWith("%")) { + try { + // Evaluate the expression, this resolves unknowns in the value. + List evaluated = fpe.evaluate(null, sd, sd, ed, node); + + if (!evaluated.isEmpty()) + constantV = evaluated.get(0).primitiveValue(); + } + catch (Exception e) { + debug("Failed to evaluate constant expression: " + constantV); + } + } + + translatedShEx += positionParts(innerShEx, quoteThis(constantV, toQt), + getNextOps(ops , processExpressionNode(sd, ed, node.getOpNext(), toQuote, depth), endOps, treatBothOpsSame), + depth, false); + //translatedShEx += positionParts(innerShEx, node.getConstant().primitiveValue(), ops + processExpressionNode(node.getOpNext(), toQuote, 0) + endOps, depth); + } else if (kind == ExpressionNode.Kind.Unary) { + translatedShEx += positionParts(innerShEx, node.getName(), + getNextOps(ops,processExpressionNode(sd, ed, node.getOpNext(), toQuote, depth), endOps, treatBothOpsSame), + depth, false); + } else { + translatedShEx += positionParts(innerShEx, node.toString(), + getNextOps(ops, processExpressionNode(sd, ed, node.getOpNext(), toQuote, depth), endOps, treatBothOpsSame), + depth, false); + } + + return translatedShEx; + } + + private String addUnmappedFunction(String func) { + String toStore = "UNMAPPED_FUNCTION_" + func; + if (!unMappedFunctions.contains(toStore)) + unMappedFunctions.add(toStore); + + return toStore; + } + + private String getNextOps(String startOp, String opNext, String endOp, boolean treatBothOps){ + if (treatBothOps) + return startOp + opNext + " " + endOp + opNext; + + return startOp + opNext + endOp; + } + + private String positionParts(String funCall, String mainTxt, String nextText, int depth, boolean complete){ + if (funCall.indexOf("CALLER") != -1) { + if (depth == 0) { + String toReturn = funCall.replaceFirst("CALLER", mainTxt); + if (complete) + toReturn = toReturn ; + + toReturn = postProcessing(toReturn, nextText); + return toReturn.replaceAll("CALLER", ""); + } + else{ + return postProcessing(funCall.replaceFirst("CALLER", "CALLER " + mainTxt ), nextText) ; + } + } + + String fc = funCall; + if (fc.startsWith("fhir:")) + fc = "." + fc.substring("fhir:".length()); + + if ((depth == 0)&&(complete)) { + if (mainTxt.startsWith("fhir:")) { + if ("".equals(funCall)) { + return "{ " + postProcessing(mainTxt, nextText) + " }"; + } + return postProcessing("{ " + mainTxt + fc + " }", nextText); + } + mainTxt = "(" + mainTxt + ")"; + if ("".equals(funCall)) { + return postProcessing(mainTxt, nextText); + } + } + + return postProcessing(mainTxt + fc, nextText); + } + + private String postProcessing(String p, String q){ + String qp = q; + if ((q != null)&&(q.trim().startsWith("XOR"))){ + qp = q.split("XOR")[1]; + + // because p xor q = ( p and not q) OR (not p and q) + return "(" + p + " AND NOT " + qp + ") OR ( NOT " + p + " AND " + qp + ")"; + } + + return p + qp; + } + + /** + * @param str + * @return + */ + private String TBD(String str){ + return " SHEX_" + str + "_SHEX "; + } + + /** + * @param str + * @param quote + * @return + */ + private String quoteThis(String str, boolean quote){ + + if (quote) + return "'" + str + "'"; + + return str; + } /** * Generate a flattened definition for the inner types @@ -398,7 +1018,9 @@ public class ShExGenerator { StringBuilder itDefs = new StringBuilder(); while(emittedInnerTypes.size() < innerTypes.size()) { for (Pair it : new HashSet>(innerTypes)) { - if (!emittedInnerTypes.contains(it)) { + if ((!emittedInnerTypes.contains(it)) + // && (it.getRight().hasBase() && it.getRight().getBase().getPath().startsWith(it.getLeft().getName())) + ){ itDefs.append("\n").append(genInnerTypeDef(it.getLeft(), it.getRight())); emittedInnerTypes.add(it); } @@ -407,6 +1029,22 @@ public class ShExGenerator { return itDefs.toString(); } + /** + * @param ed + * @return + */ + private boolean isInInnerTypes(ElementDefinition ed) { + + if (this.innerTypes.isEmpty()) + return false; + + for (Iterator> itr = this.innerTypes.iterator(); itr.hasNext(); ) + if (itr.next().getRight() == ed) + return true; + + return false; + } + /** * Generate a shape definition for the current set of datatypes * @return stringified data type definitions @@ -417,7 +1055,7 @@ public class ShExGenerator { for (String dt : new HashSet(datatypes)) { if (!emittedDatatypes.contains(dt)) { StructureDefinition sd = context.fetchResource(StructureDefinition.class, - ProfileUtilities.sdNs(dt, null)); + ProfileUtilities.sdNs(dt, null)); // TODO: Figure out why the line below doesn't work // if (sd != null && !uniq_structures.contains(sd)) if(sd != null && !uniq_structure_urls.contains(sd.getUrl())) @@ -429,6 +1067,11 @@ public class ShExGenerator { return dtDefs.toString(); } + /** + * @param text + * @param max_col + * @return + */ private ArrayList split_text(String text, int max_col) { int pos = 0; ArrayList rval = new ArrayList(); @@ -452,6 +1095,10 @@ public class ShExGenerator { return rval; } + /** + * @param tmplt + * @param ed + */ private void addComment(ST tmplt, ElementDefinition ed) { if(withComments && ed.hasShort() && !ed.getId().startsWith("Extension.")) { int nspaces; @@ -482,63 +1129,143 @@ public class ShExGenerator { * @param ed Containing element definition * @return ShEx definition */ - private String genElementDefinition(StructureDefinition sd, ElementDefinition ed) { + private String genElementDefinition(StructureDefinition sd, + ElementDefinition ed) { String id = ed.hasBase() ? ed.getBase().getPath() : ed.getPath(); - String shortId = id.substring(id.lastIndexOf(".") + 1); - String defn; + + String shortId = id; + String typ = id; + + if (id.equals("Element.extension") && ed.hasSliceName()) { + shortId = ed.getSliceName(); + if (ed.getType().size() > 0) + typ = ed.getType().get(0).getCode(); + } + else + shortId = id.substring(id.lastIndexOf(".") + 1); + + if ((ed.getType().size() > 0) && + (ed.getType().get(0).getCode().startsWith(Constants.NS_SYSTEM_TYPE))) { + shortId = "v"; + } + + String defn = ""; ST element_def; String card = ("*".equals(ed.getMax()) ? (ed.getMin() == 0 ? "*" : "+") : (ed.getMin() == 0 ? "?" : "")) + ";"; - if(id.endsWith("[x]")) { - element_def = ed.getType().size() > 1? tmplt(INNER_SHAPE_TEMPLATE) : tmplt(ELEMENT_TEMPLATE); - element_def.add("id", ""); + element_def = tmplt(ELEMENT_TEMPLATE); + if (id.endsWith("[x]")) { + element_def.add("id", "fhir:" + shortId.replace("[x]", "")); } else { - element_def = tmplt(ELEMENT_TEMPLATE); - element_def.add("id", "fhir:" + (id.charAt(0) == id.toLowerCase().charAt(0)? shortId : id) + " "); + element_def.add("id", "fhir:" + shortId + " "); } List children = profileUtilities.getChildList(sd, ed); if (children.size() > 0) { - innerTypes.add(new ImmutablePair(sd, ed)); - defn = simpleElement(sd, ed, id); - } else if(id.endsWith("[x]")) { - defn = genChoiceTypes(sd, ed, id); + String parentPath = sd.getName(); + if ((ed.hasContentReference() && (!ed.hasType())) || (!id.equals(parentPath + "." + shortId))) { + //debug("Not Adding innerType:" + id + " to " + sd.getName()); + } else + innerTypes.add(new ImmutablePair(sd, ed)); } - else if (ed.getType().size() == 1) { - // Single entry - defn = genTypeRef(sd, ed, id, ed.getType().get(0)); - } else if (ed.getContentReference() != null) { - // Reference to another element - String ref = ed.getContentReference(); - if(!ref.startsWith("#")) - throw new AssertionError("Not equipped to deal with absolute path references: " + ref); - String refPath = null; - for(ElementDefinition ed1: sd.getSnapshot().getElement()) { - if(ed1.getId() != null && ed1.getId().equals(ref.substring(1))) { - refPath = ed1.getPath(); - break; + + defn = simpleElement(sd, ed, typ); + + String refChoices = ""; + + if (id.endsWith("[x]")) { + defn = " (" + genChoiceTypes(sd, ed, shortId) + ") "; + defn += " AND { rdf:type IRI } "; + } else { + if (ed.getType().size() == 1) { + // Single entry + if (defn.isEmpty()) + defn = genTypeRef(sd, ed, id, ed.getType().get(0)); + } else if (ed.getContentReference() != null) { + // Reference to another element + String ref = ed.getContentReference(); + if (!ref.startsWith("#")) + throw new AssertionError("Not equipped to deal with absolute path references: " + ref); + String refPath = null; + for (ElementDefinition ed1 : sd.getSnapshot().getElement()) { + if (ed1.getId() != null && ed1.getId().equals(ref.substring(1))) { + refPath = ed1.getPath(); + break; + } + } + if (refPath == null) + throw new AssertionError("Reference path not found: " + ref); + + defn = simpleElement(sd, ed, refPath); + } + + + List refValues = new ArrayList(); + if (ed.hasType() && (ed.getType().get(0).getWorkingCode().equals("Reference"))) { + if (ed.getType().get(0).hasTargetProfile()) { + + ed.getType().get(0).getTargetProfile().forEach((CanonicalType tps) -> { + String els[] = tps.getValue().split("/"); + refValues.add(els[els.length - 1]); + }); } } - if(refPath == null) - throw new AssertionError("Reference path not found: " + ref); - // String typ = id.substring(0, id.indexOf(".") + 1) + ed.getContentReference().substring(1); - defn = simpleElement(sd, ed, refPath); - } else if(id.endsWith("[x]")) { - defn = genChoiceTypes(sd, ed, id); - } else { - // TODO: Refactoring required here - element_def = genAlternativeTypes(ed, id, shortId); - element_def.add("id", id.charAt(0) == id.toLowerCase().charAt(0)? shortId : id); - element_def.add("card", card); - addComment(element_def, ed); - return element_def.render(); + + if (!refValues.isEmpty()) { + Collections.sort(refValues); + refChoices = StringUtils.join(refValues, "_OR_"); + } } + + // Adding OneOrMore as prefix to the reference type if cardinality is 1..* or 0..* + if (card.startsWith("*") || card.startsWith("+")) { + card = card.replace("+", ""); + card = card.replace("*", "?"); + defn = defn.replace("<", "<" + ONE_OR_MORE_PREFIX); + + String defnToStore = defn; + if (!refChoices.isEmpty()) { + defnToStore = defn.replace(">", ONE_OR_MORE_CHOICES + refChoices + ">"); + defn = defn.replace(">", "_" + refChoices + ">"); + } + + defnToStore = StringUtils.substringBetween(defnToStore, "<", ">"); + if (!oneOrMoreTypes.contains(defnToStore)) + oneOrMoreTypes.add(defnToStore); + } else { + if (!refChoices.isEmpty()) { + defn += " AND {fhir:link \n\t\t\t@<" + + refChoices.replaceAll("_OR_", "> OR \n\t\t\t@<") + "> }"; + } + } + element_def.add("defn", defn); element_def.add("card", card); addComment(element_def, ed); + return element_def.render(); } + private List getChildren(StructureDefinition derived, ElementDefinition element) { + List elements = derived.getSnapshot().getElement(); + int index = elements.indexOf(element) + 1; + String path = element.getPath()+"."; + List list = new ArrayList<>(); + while (index < elements.size()) { + ElementDefinition e = elements.get(index); + String p = e.getPath(); + if (p.startsWith(path) && !e.hasSliceName()) { + if (!p.substring(path.length()).contains(".")) { + list.add(e); + } + index++; + } else { + break; + } + } + return list; + } + /** * Generate a type reference and optional value set definition * @param sd Containing StructureDefinition @@ -595,7 +1322,7 @@ public class ShExGenerator { } else if (typ.getCode().startsWith(Constants.NS_SYSTEM_TYPE)) { String xt = getShexCode(typ.getWorkingCode()); - + // TODO: Remove the next line when the type of token gets switched to string // TODO: Add a rdf-type entry for valueInteger to xsd:integer (instead of int) ST td_entry = tmplt(PRIMITIVE_ELEMENT_DEFN_TEMPLATE).add("typ", xt); @@ -632,51 +1359,55 @@ public class ShExGenerator { } } + /** + * @param c + * @return + */ private String getShexCode(String c) { switch (c) { - case "boolean" : - return "xsd:boolean"; - case "integer" : - return "xsd:int"; - case "integer64" : - return "xsd:long"; - case "decimal" : - return "xsd:decimal, xsd:double"; - case "base64Binary" : - return "xsd:base64Binary"; - case "instant" : - return "xsd:dateTime"; - case "string" : - return "xsd:string"; - case "uri" : - return "xsd:anyURI"; - case "date" : - return "xsd:gYear, xsd:gYearMonth, xsd:date"; - case "dateTime" : - return "xsd:gYear, xsd:gYearMonth, xsd:date, xsd:dateTime"; - case "time" : - return "xsd:time"; - case "code" : - return "xsd:token"; - case "oid" : - return "xsd:anyURI"; - case "uuid" : - return "xsd:anyURI"; - case "url" : - return "xsd:anyURI"; - case "canonical" : - return "xsd:anyURI"; - case "id" : - return "xsd:string"; - case "unsignedInt" : - return "xsd:nonNegativeInteger"; - case "positiveInt" : - return "xsd:positiveInteger"; - case "markdown" : - return "xsd:string"; + case "boolean" : + return "xsd:boolean"; + case "integer" : + return "xsd:int"; + case "integer64" : + return "xsd:long"; + case "decimal" : + return "xsd:decimal OR xsd:double"; + case "base64Binary" : + return "xsd:base64Binary"; + case "instant" : + return "xsd:dateTime"; + case "string" : + return "xsd:string"; + case "uri" : + return "xsd:anyURI"; + case "date" : + return "xsd:gYear OR xsd:gYearMonth OR xsd:date"; + case "dateTime" : + return "xsd:gYear OR xsd:gYearMonth OR xsd:date OR xsd:dateTime"; + case "time" : + return "xsd:time"; + case "code" : + return "xsd:token"; + case "oid" : + return "xsd:anyURI"; + case "uuid" : + return "xsd:anyURI"; + case "url" : + return "xsd:anyURI"; + case "canonical" : + return "xsd:anyURI"; + case "id" : + return "xsd:string"; + case "unsignedInt" : + return "xsd:nonNegativeInteger"; + case "positiveInt" : + return "xsd:positiveInteger"; + case "markdown" : + return "xsd:string"; } throw new Error("Not implemented yet"); - + } /** @@ -694,7 +1425,7 @@ public class ShExGenerator { for(ElementDefinition.TypeRefComponent typ : ed.getType()) { altEntries.add(genAltEntry(id, typ)); } - shex_alt.add("altEntries", StringUtils.join(altEntries, " OR\n ")); + shex_alt.add("altEntries", StringUtils.join(altEntries, " OR \n ")); return shex_alt; } @@ -720,14 +1451,29 @@ public class ShExGenerator { * @param id choice identifier * @return ShEx fragment for the set of choices */ - private String genChoiceTypes(StructureDefinition sd, ElementDefinition ed, String id) { + private String genChoiceTypes(StructureDefinition sd, + ElementDefinition ed, + String id) { List choiceEntries = new ArrayList(); + List refValues = new ArrayList(); String base = id.replace("[x]", ""); - for(ElementDefinition.TypeRefComponent typ : ed.getType()) - choiceEntries.add(genChoiceEntry(sd, ed, id, base, typ)); + for (ElementDefinition.TypeRefComponent typ : ed.getType()) { + String entry = genChoiceEntry(sd, ed, "", "", typ); + refValues.clear(); + if (typ.hasTargetProfile()) { + typ.getTargetProfile().forEach((CanonicalType tps) -> { + String els[] = tps.getValue().split("/"); + refValues.add("@<" + els[els.length - 1] + ">"); + }); + } - return StringUtils.join(choiceEntries, " |\n"); + if (!refValues.isEmpty()) + choiceEntries.add("(" + entry + " AND {fhir:link " + StringUtils.join(refValues, " OR \n\t\t\t ") + " }) "); + else + choiceEntries.add(entry); + } + return StringUtils.join(choiceEntries, " OR \n\t\t\t"); } /** @@ -736,17 +1482,52 @@ public class ShExGenerator { * @param typ type/discriminant * @return ShEx fragment for choice entry */ - private String genChoiceEntry(StructureDefinition sd, ElementDefinition ed, String id, String base, ElementDefinition.TypeRefComponent typ) { + private String genChoiceEntry(StructureDefinition sd, + ElementDefinition ed, + String id, + String base, + ElementDefinition.TypeRefComponent typ) { ST shex_choice_entry = tmplt(ELEMENT_TEMPLATE); String ext = typ.getWorkingCode(); - shex_choice_entry.add("id", "fhir:" + base+Character.toUpperCase(ext.charAt(0)) + ext.substring(1) + " "); + // shex_choice_entry.add("id", "fhir:" + base+Character.toUpperCase(ext.charAt(0)) + ext.substring(1) + " "); + shex_choice_entry.add("id", ""); shex_choice_entry.add("card", ""); shex_choice_entry.add("defn", genTypeRef(sd, ed, id, typ)); shex_choice_entry.add("comment", " "); return shex_choice_entry.render(); } + /** + * @param oneOrMoreType + * @return + */ + private String getOneOrMoreType(String oneOrMoreType) { + if ((oneOrMoreType == null)||(oneOrMoreTypes.isEmpty())) + return ""; + + ST one_or_more_type = tmplt(ONE_OR_MORE_TEMPLATE); + String oomType = oneOrMoreType; + String origType = oneOrMoreType; + String restriction = ""; + if (oneOrMoreType.indexOf(ONE_OR_MORE_CHOICES) != -1) { + oomType = oneOrMoreType.replaceAll(ONE_OR_MORE_CHOICES, "_"); + origType = oneOrMoreType.split(ONE_OR_MORE_CHOICES)[0]; + restriction = "AND {fhir:link \n\t\t\t@<"; + + String choices = oneOrMoreType.split(ONE_OR_MORE_CHOICES)[1]; + restriction += choices.replaceAll("_OR_", "> OR \n\t\t\t@<") + "> }"; + } + + origType = origType.replaceAll(ONE_OR_MORE_PREFIX, ""); + + one_or_more_type.add("oomType", oomType); + one_or_more_type.add("origType", origType); + one_or_more_type.add("restriction", restriction); + one_or_more_type.add("comment", ""); + return one_or_more_type.render(); + } + /** * Generate a definition for a referenced element * @param sd Containing structure definition @@ -754,18 +1535,59 @@ public class ShExGenerator { * @return ShEx representation of element reference */ private String genInnerTypeDef(StructureDefinition sd, ElementDefinition ed) { - String path = ed.hasBase() ? ed.getBase().getPath() : ed.getPath();; + String path = ed.hasBase() ? ed.getBase().getPath() : ed.getPath(); ST element_reference = tmplt(SHAPE_DEFINITION_TEMPLATE); element_reference.add("resourceDecl", ""); // Not a resource - element_reference.add("id", path); + element_reference.add("id", path + getExtendedType(ed)); + element_reference.add("fhirType", " "); String comment = ed.getShort(); element_reference.add("comment", comment == null? " " : "# " + comment); List elements = new ArrayList(); for (ElementDefinition child: profileUtilities.getChildList(sd, path, null)) - elements.add(genElementDefinition(sd, child)); + if (child.hasBase() && child.getBase().getPath().startsWith(sd.getName())) { + String elementDefinition = genElementDefinition(sd, child); + elements.add(elementDefinition); + } element_reference.add("elements", StringUtils.join(elements, "\n")); + + List innerConstraintsList = new ArrayList(); + // Process constraints + for (ElementDefinition.ElementDefinitionConstraintComponent constraint : ed.getConstraint()) { + String sdType = sd.getType(); + String cstype = constraint.getSource(); + if ((!cstype.isEmpty()) && (cstype.indexOf("/") != -1)) { + String[] els = cstype.split("/"); + cstype = els[els.length - 1]; + } + + String id = ed.hasBase() ? ed.getBase().getPath() : ed.getPath(); + String shortId = id.substring(id.lastIndexOf(".") + 1); + if ((ed.hasContentReference() && (!ed.hasType())) || (id.equals(sd.getName() + "." + shortId))) { + if ((sdType.equals(cstype)) || baseDataTypes.contains(sdType)) { + //if (!isInInnerTypes(ed)) { + debug("\n (INNER ED) Key: " + constraint.getKey() + " SD type: " + sd.getType() + " Element: " + ed.getPath() + " Constraint Source: " + constraint.getSource() + " Constraint:" + constraint.getExpression()); + String transl = translateConstraint(sd, ed, constraint); + if (transl.isEmpty() || innerConstraintsList.contains(transl)) + continue; + innerConstraintsList.add(transl); + //} + } + } + } + + String constraintStr = ""; + + if (!innerConstraintsList.isEmpty()) { + constraintStr = "AND (\n\n" + StringUtils.join(constraintsList, "\n\n) AND (\n\n") + "\n\n)\n"; + } + + element_reference.add("constraints", constraintStr); + + // TODO: See if we need to process contexts + element_reference.add("contextOfUse", ""); + return element_reference.render(); } @@ -803,14 +1625,18 @@ public class ShExGenerator { } } + /** + * @param vs + * @return + */ private String genValueSet(ValueSet vs) { ST vsd = tmplt(VALUE_SET_DEFINITION).add("vsuri", vsprefix(vs.getUrl())).add("comment", vs.getDescription()); ValueSetExpander.ValueSetExpansionOutcome vse = context.expandVS(vs, true, false); List valid_codes = new ArrayList(); if(vse != null && - vse.getValueset() != null && - vse.getValueset().hasExpansion() && - vse.getValueset().getExpansion().hasContains()) { + vse.getValueset() != null && + vse.getValueset().hasExpansion() && + vse.getValueset().getExpansion().hasContains()) { for(ValueSet.ValueSetExpansionContainsComponent vsec : vse.getValueset().getExpansion().getContains()) valid_codes.add("\"" + vsec.getCode() + "\""); } @@ -818,6 +1644,11 @@ public class ShExGenerator { } + /** + * @param ctxt + * @param reference + * @return + */ // TODO: find a utility that implements this private ValueSet resolveBindingReference(DomainResource ctxt, String reference) { try { @@ -826,4 +1657,13 @@ public class ShExGenerator { return null; } } + + private void debug(String message) { + if (this.debugMode) + System.out.println(message); + } + + private void printBuildMessage(String message){ + System.out.println("ShExGenerator: " + message); + } } \ No newline at end of file