diff --git a/org.hl7.fhir.r5/pom.xml b/org.hl7.fhir.r5/pom.xml index 3c2f4a34b..164a32acd 100644 --- a/org.hl7.fhir.r5/pom.xml +++ b/org.hl7.fhir.r5/pom.xml @@ -50,6 +50,13 @@ true + + + es.weso + shexs_2.12 + 0.2.31 + + com.google.code.gson 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..13a656b7e 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 boolean doDatatypes = true; // add data types + + 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 = false; // 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,74 +340,184 @@ 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 data types are to be put in the same file if(doDatatypes) { shapeDefinitions.append("\n#---------------------- Data Types -------------------\n"); while (emittedDatatypes.size() < datatypes.size() || - emittedInnerTypes.size() < innerTypes.size()) { + emittedInnerTypes.size() < innerTypes.size()) { shapeDefinitions.append(emitDataTypes()); + // As process data types, it may introduce some more inner types, so we repeat the call here. shapeDefinitions.append(emitInnerTypes()); } } - shapeDefinitions.append("\n#---------------------- Reference Types -------------------\n"); - for(String r: references) { - shapeDefinitions.append("\n").append(tmplt(TYPED_REFERENCE_TEMPLATE).add("refType", r).render()).append("\n"); - if (!"Resource".equals(r) && !known_resources.contains(r)) - shapeDefinitions.append("\n").append(tmplt(TARGET_REFERENCE_TEMPLATE).add("refType", r).render()).append("\n"); + if (oneOrMoreTypes.size() > 0) { + shapeDefinitions.append("\n#---------------------- Cardinality Types (OneOrMore) -------------------\n"); + oneOrMoreTypes.forEach((String oomType) -> { + shapeDefinitions.append(getOneOrMoreType(oomType)); + }); } + + if (references.size() > 0) { + shapeDefinitions.append("\n#---------------------- Reference Types -------------------\n"); + for (String r : references) { + shapeDefinitions.append("\n").append(tmplt(TYPED_REFERENCE_TEMPLATE).add("refType", r).render()).append("\n"); + if (!"Resource".equals(r) && !known_resources.contains(r)) + shapeDefinitions.append("\n").append(tmplt(TARGET_REFERENCE_TEMPLATE).add("refType", r).render()).append("\n"); + } + } + shex_def.add("shapeDefinitions", shapeDefinitions); 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()); + } + + if (required_value_sets.size() > 0) { + 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); + } } - shapeDefinitions.append("\n#---------------------- Value Sets ------------------------\n"); - for(ValueSet vs: required_value_sets) - shapeDefinitions.append("\n").append(genValueSet(vs)); 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 +532,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 +560,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 +1028,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 +1039,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 +1065,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 +1077,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 +1105,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 +1139,145 @@ 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 (ed.getType().size() > 0) + typ = ed.getType().get(0).getCode(); + + if (id.equals("Element.extension") && ed.hasSliceName()) { + shortId = ed.getSliceName(); + } + else + shortId = id.substring(id.lastIndexOf(".") + 1); + + if ((ed.getType().size() > 0) && + (ed.getType().get(0).getCode().startsWith(Constants.NS_SYSTEM_TYPE))) { + shortId = "v"; + typ = ed.getType().get(0).getWorkingCode(); + } + + 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())||(typ.equals(sd.getName()))) + 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 +1334,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 +1371,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 +1437,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 +1463,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 +1494,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 +1547,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 +1637,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 +1656,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 +1669,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 diff --git a/org.hl7.fhir.r5/src/test/java/org/hl7/fhir/r5/test/ShexGeneratorTestUtils.java b/org.hl7.fhir.r5/src/test/java/org/hl7/fhir/r5/test/ShexGeneratorTestUtils.java new file mode 100644 index 000000000..7e210bc1f --- /dev/null +++ b/org.hl7.fhir.r5/src/test/java/org/hl7/fhir/r5/test/ShexGeneratorTestUtils.java @@ -0,0 +1,168 @@ +package org.hl7.fhir.r5.test; + +import org.hl7.fhir.r5.model.StructureDefinition; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +public class ShexGeneratorTestUtils { + public class resDef { + public String name; + public String url; + public String info; + + public resDef(String _name, String _url, String _info){ + this.name = _name; + this.url = _url; + this.info = _info; + } + + @Override + public String toString() { + return " " + name + "[ " + url + " ] "; + } + } + + public enum RESOURCE_CATEGORY{ + LOGICAL_NAMES, STRUCTURE_DEFINITIONS, EXTENSIONS, PROFILES, ALL + } + public List getSDs(List sds, RESOURCE_CATEGORY cat) { + List selSDs = new ArrayList(); + sds.forEach((StructureDefinition sd) -> { + switch(cat) { + case STRUCTURE_DEFINITIONS: + if (sd.getType().trim().equals(sd.getName().trim())) + selSDs.add(new resDef(sd.getName(), sd.getUrl(), getSDInfo(sd))); + break; + case LOGICAL_NAMES: + if (sd.getBaseDefinition() == null) + selSDs.add(new resDef(sd.getName(), sd.getUrl(), getSDInfo(sd))); + break; + case EXTENSIONS: + if ("Extension".equals(sd.getType())) + selSDs.add(new resDef(sd.getName(), sd.getUrl(), getSDInfo(sd))); + break; + case PROFILES: + if (!((sd.getBaseDefinition() == null) || + ("Extension".equals(sd.getType())) || + (sd.getType().trim().equals(sd.getName().trim())))) + selSDs.add(new resDef(sd.getName(), sd.getUrl(), getSDInfo(sd))); + break; + default: + selSDs.add(new resDef(sd.getName(), sd.getUrl(), getSDInfo(sd))); + } + }); + + Collections.sort(selSDs, new Comparator() { + @Override + public int compare(resDef o1, resDef o2) { + return o1.name.compareTo(o2.name); + } + }); + + return selSDs; + } + public static List getMetaStructureDefinitionsToSkip(){ + List skipSDs = new ArrayList(); + skipSDs.add("http://hl7.org/fhir/StructureDefinition/ActivityDefinition"); + skipSDs.add("http://hl7.org/fhir/StructureDefinition/AdministrableProductDefinition"); + skipSDs.add("http://hl7.org/fhir/StructureDefinition/ChargeItemDefinition"); + skipSDs.add("http://hl7.org/fhir/StructureDefinition/ClinicalUseDefinition"); + skipSDs.add("http://hl7.org/fhir/StructureDefinition/CodeSystem"); + skipSDs.add("http://hl7.org/fhir/StructureDefinition/ConceptMap"); + skipSDs.add("http://hl7.org/fhir/StructureDefinition/CompartmentDefinition"); + skipSDs.add("http://hl7.org/fhir/StructureDefinition/CompartmentDefinition"); + skipSDs.add("http://hl7.org/fhir/StructureDefinition/DeviceDefinition"); + skipSDs.add("http://hl7.org/fhir/StructureDefinition/ElementDefinition"); + skipSDs.add("http://hl7.org/fhir/StructureDefinition/EventDefinition"); + skipSDs.add("http://hl7.org/fhir/StructureDefinition/GraphDefinition"); + skipSDs.add("http://hl7.org/fhir/StructureDefinition/GuidanceResponse"); + skipSDs.add("http://hl7.org/fhir/StructureDefinition/Library"); + skipSDs.add("http://hl7.org/fhir/StructureDefinition/ManufacturedItemDefinition"); + skipSDs.add("http://hl7.org/fhir/StructureDefinition/Measure"); + skipSDs.add("http://hl7.org/fhir/StructureDefinition/MeasureReport"); + skipSDs.add("http://hl7.org/fhir/StructureDefinition/MedicinalProductDefinition"); + skipSDs.add("http://hl7.org/fhir/StructureDefinition/MessageDefinition"); + skipSDs.add("http://hl7.org/fhir/StructureDefinition/NamingSystem"); + skipSDs.add("http://hl7.org/fhir/StructureDefinition/ObservationDefinition"); + skipSDs.add("http://hl7.org/fhir/StructureDefinition/OperationDefinition"); + skipSDs.add("http://hl7.org/fhir/StructureDefinition/PackagedProductDefinition"); + skipSDs.add("http://hl7.org/fhir/StructureDefinition/ParameterDefinition"); + skipSDs.add("http://hl7.org/fhir/StructureDefinition/PlanDefinition"); + skipSDs.add("http://hl7.org/fhir/StructureDefinition/SearchParameter"); + skipSDs.add("http://hl7.org/fhir/StructureDefinition/SpecimenDefinition"); + skipSDs.add("http://hl7.org/fhir/StructureDefinition/StructureDefinition"); + skipSDs.add("http://hl7.org/fhir/StructureDefinition/SubstanceDefinition"); + skipSDs.add("http://hl7.org/fhir/StructureDefinition/Task"); + skipSDs.add("http://hl7.org/fhir/StructureDefinition/TerminologyCapabilities"); + skipSDs.add("http://hl7.org/fhir/StructureDefinition/TriggerDefinition"); + skipSDs.add("http://hl7.org/fhir/StructureDefinition/ValueSet"); + return skipSDs; + } + + /** + * This method prepares selected extensions to test the policy in which ShEx Generator + * only translates selected resource extensions + * @return List List of selected resource extensions for testing + */ + public static List getSelectedExtensions(){ + List selectedExtesnsions = new ArrayList(); + selectedExtesnsions.add("http://fhir-registry.smarthealthit.org/StructureDefinition/capabilities"); + selectedExtesnsions.add("http://fhir-registry.smarthealthit.org/StructureDefinition/oauth-uris"); + return selectedExtesnsions; + } + + public static String getSDInfo(StructureDefinition sd) { + if (sd != null) { + String kind = " "; + try { + kind = sd.getKind().name(); + } catch (Exception e) { + System.out.println("Kind is null"); + } + String name = " "; + try { + name = sd.getName(); + } catch (Exception e) { + System.out.println("Name is null"); + } + String type = " "; + try { + type = sd.getType(); + } catch (Exception e) { + System.out.println("Type is null"); + } + String derv = " "; + try { + derv = sd.getDerivation().name(); + } catch (Exception e) { + System.out.println("Derivation is null"); + } + String url = " "; + try { + url = sd.getUrl(); + } catch (Exception e) { + System.out.println("URL is null"); + } + String base = " "; + try { + base = sd.getBaseDefinition(); + } catch (Exception e) { + System.out.println("Base is null"); + } + return kind + "\t" + name + "\t" + type + "\t" + derv + "\t" + url + "\t" + base; + } + return ""; + } + + public static void printList(String title, List items) { + System.out.println("************************************************************************"); + System.out.println("Printing " + title); + System.out.println("************************************************************************"); + items.forEach((resDef item) -> { + System.out.println(item.name + " [" + item.url + "]"); + }); + } +} diff --git a/org.hl7.fhir.r5/src/test/java/org/hl7/fhir/r5/test/ShexGeneratorTests.java b/org.hl7.fhir.r5/src/test/java/org/hl7/fhir/r5/test/ShexGeneratorTests.java index 78cf1b8ca..3c66045b6 100644 --- a/org.hl7.fhir.r5/src/test/java/org/hl7/fhir/r5/test/ShexGeneratorTests.java +++ b/org.hl7.fhir.r5/src/test/java/org/hl7/fhir/r5/test/ShexGeneratorTests.java @@ -4,18 +4,37 @@ import java.io.FileNotFoundException; import java.io.IOException; import java.nio.file.FileSystems; import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import es.weso.shex.Schema; +import es.weso.shex.validator.ShExsValidator; +import es.weso.shex.validator.ShExsValidatorBuilder; +import org.apache.commons.lang3.StringUtils; import org.fhir.ucum.UcumException; import org.hl7.fhir.exceptions.FHIRException; import org.hl7.fhir.r5.conformance.profile.ProfileUtilities; import org.hl7.fhir.r5.conformance.ShExGenerator; import org.hl7.fhir.r5.conformance.ShExGenerator.HTMLLinkPolicy; +import org.hl7.fhir.r5.context.IWorkerContext; import org.hl7.fhir.r5.model.StructureDefinition; import org.hl7.fhir.r5.test.utils.TestingUtilities; import org.hl7.fhir.utilities.TextFile; +import org.junit.Assert; +import org.junit.Ignore; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; +import static org.hl7.fhir.r5.test.ShexGeneratorTestUtils.printList; + public class ShexGeneratorTests { + public static List selectedExtesnsions = new ArrayList(); + + public ShExGenerator shexGenerator; + + @BeforeAll + public static void setup() { + } private void doTest(String name) throws FileNotFoundException, IOException, FHIRException, UcumException { StructureDefinition sd = TestingUtilities.getSharedWorkerContext().fetchResource(StructureDefinition.class, ProfileUtilities.sdNs(name, null)); @@ -24,8 +43,11 @@ public class ShexGeneratorTests { } Path outPath = FileSystems.getDefault().getPath(System.getProperty("java.io.tmpdir"), name.toLowerCase() + ".shex"); TextFile.stringToFile(new ShExGenerator(TestingUtilities.getSharedWorkerContext()).generate(HTMLLinkPolicy.NONE, sd), outPath.toString()); - } + // For Testing Schema Processing and Constraint Mapping related Development + // If you un-comment the following lines, please comment all other lines in this method. + //this.doTestThis(name.toLowerCase(), name, false, ShExGenerator.ConstraintTranslationPolicy.ALL, false, true); + } @Test public void testId() throws FHIRException, IOException, UcumException { doTest("id"); @@ -75,4 +97,193 @@ public class ShexGeneratorTests { public void testSignature() throws FHIRException, IOException, UcumException { doTest("Signature"); } + + @Test + public void testString() throws FHIRException, IOException, UcumException { + doTest("string"); + } + + private void doTestThis(String shortName, String name, boolean useSelectedExtensions, ShExGenerator.ConstraintTranslationPolicy policy, boolean debugMode, boolean validateShEx) { + IWorkerContext ctx = TestingUtilities.getSharedWorkerContext(); + StructureDefinition sd = ctx.fetchResource(StructureDefinition.class, ProfileUtilities.sdNs(name, null)); + if (sd == null) { + throw new FHIRException("StructuredDefinition for " + name + " was null"); + } + //Path outPath = FileSystems.getDefault().getPath(System.getProperty("java.io.tmpdir"), name.toLowerCase() + ".shex"); + Path outPath = FileSystems.getDefault().getPath(System.getProperty("user.home") + "/runtime_environments/ShExSchemas", shortName + ".shex"); + try { + this.shexGenerator = new ShExGenerator(ctx); + + this.shexGenerator.debugMode = debugMode; + this.shexGenerator.constraintPolicy = policy; + + // ShEx Generator skips resources which are at Meta level of FHIR Resource definitions + this.shexGenerator.setExcludedStructureDefinitionUrls( + ShexGeneratorTestUtils.getMetaStructureDefinitionsToSkip()); + + // when ShEx translates only selected resource extensions + if (useSelectedExtensions) { + List selExtns = new ArrayList(); + for (String eUrl : ShexGeneratorTestUtils.getSelectedExtensions()) { + StructureDefinition esd = ctx.fetchResource(StructureDefinition.class, ProfileUtilities.sdNs(eUrl, null)); + if (esd != null) + selExtns.add(esd); + } + this.shexGenerator.setSelectedExtension(selExtns); + } + + String schema = this.shexGenerator.generate(HTMLLinkPolicy.NONE, sd); + if (!schema.isEmpty()) { + + if (validateShEx) { + try { + ShExsValidator validator = ShExsValidatorBuilder.fromStringSync(schema, "ShexC"); + Schema sch = validator.schema(); + + Assert.assertNotNull(sch); + System.out.println("VALIDATION PASSED for ShEx Schema " + sd.getName()); + } catch (Exception e) { + System.out.println("VALIDATION FAILED for ShEx Schema " + sd.getName()); + //System.out.println("\t\t\tMessage: " + e.getMessage()); + } + } + TextFile.stringToFile(schema, outPath.toString()); + } + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + @Ignore + public void doTestAll() throws FileNotFoundException, IOException, FHIRException, UcumException { + List sds = TestingUtilities.getSharedWorkerContext().fetchResourcesByType(StructureDefinition.class); + + processSDList( + ShexGeneratorTestUtils.RESOURCE_CATEGORY.ALL, // Processing All kinds of Structure Definitions + sds, // List of Structure Definitions + false, //Process all extensions + ShExGenerator.ConstraintTranslationPolicy.ALL + // Process all types of constraints, do not skip + ); + } + + @Ignore + public void doTestGenericExtensionsOnlyPolicy() throws FileNotFoundException, IOException, FHIRException, UcumException { + List sds = TestingUtilities.getSharedWorkerContext().fetchResourcesByType(StructureDefinition.class); + + processSDList( + ShexGeneratorTestUtils.RESOURCE_CATEGORY.ALL, // Processing All kinds of Structure Definitions + sds, // List of Structure Definitions + false, //Process all extensions + ShExGenerator.ConstraintTranslationPolicy.GENERIC_ONLY + // Process generic constraints only, ignore constraints of type 'context of use' + ); + + } + + @Ignore + public void doTestContextOfUseExtensionsOnlyPolicy() throws FileNotFoundException, IOException, FHIRException, UcumException { + List sds = TestingUtilities.getSharedWorkerContext().fetchResourcesByType(StructureDefinition.class); + + processSDList( + ShexGeneratorTestUtils.RESOURCE_CATEGORY.ALL, // Processing All kinds of Structure Definitions + sds, // List of Structure Definitions + false, //Process all extensions + ShExGenerator.ConstraintTranslationPolicy.CONTEXT_OF_USE_ONLY + // Process constraints only where context of use found, skip otherwise + ); + } + + @Ignore + public void doTestSelectedExtensions() throws FileNotFoundException, IOException, FHIRException, UcumException { + List sds = TestingUtilities.getSharedWorkerContext().fetchResourcesByType(StructureDefinition.class); + + processSDList( + ShexGeneratorTestUtils.RESOURCE_CATEGORY.ALL, // Processing All kinds of Structure Definitions + sds, // List of Structure Definitions + true, //Process only given/selected extensions, ignore other extensions + ShExGenerator.ConstraintTranslationPolicy.ALL // Process all type of constraints + ); + } + + @Ignore + public void testStructureDefinitionsOnly() throws FileNotFoundException, IOException, FHIRException, UcumException { + List sds = TestingUtilities.getSharedWorkerContext().fetchResourcesByType(StructureDefinition.class); + + processSDList( + ShexGeneratorTestUtils.RESOURCE_CATEGORY.STRUCTURE_DEFINITIONS, // Processing All kinds of Structure Definitions + sds, // List of Structure Definitions + false, //Process only given/selected extensions, ignore other extensions + ShExGenerator.ConstraintTranslationPolicy.ALL // Process all type of constraints + ); + } + + @Ignore + public void testExtensionsOnly() throws FileNotFoundException, IOException, FHIRException, UcumException { + List sds = TestingUtilities.getSharedWorkerContext().fetchResourcesByType(StructureDefinition.class); + + processSDList( + ShexGeneratorTestUtils.RESOURCE_CATEGORY.EXTENSIONS, // Processing All kinds of Structure Definitions + sds, // List of Structure Definitions + false, //Process only given/selected extensions, ignore other extensions + ShExGenerator.ConstraintTranslationPolicy.ALL // Process all type of constraints + ); + } + + @Ignore + public void testLogicalNamesOnly() throws FileNotFoundException, IOException, FHIRException, UcumException { + List sds = TestingUtilities.getSharedWorkerContext().fetchResourcesByType(StructureDefinition.class); + + processSDList( + ShexGeneratorTestUtils.RESOURCE_CATEGORY.LOGICAL_NAMES, // Processing All kinds of Structure Definitions + sds, // List of Structure Definitions + false, //Process only given/selected extensions, ignore other extensions + ShExGenerator.ConstraintTranslationPolicy.ALL // Process all type of constraints + ); + } + + @Ignore + public void testProfilesOnly() throws FileNotFoundException, IOException, FHIRException, UcumException { + List sds = TestingUtilities.getSharedWorkerContext().fetchResourcesByType(StructureDefinition.class); + processSDList( + ShexGeneratorTestUtils.RESOURCE_CATEGORY.PROFILES, // Processing All kinds of Structure Definitions + sds, // List of Structure Definitions + false, //Process only given/selected extensions, ignore other extensions + ShExGenerator.ConstraintTranslationPolicy.ALL // Process all type of constraints + ); + } + + private void processSDList(ShexGeneratorTestUtils.RESOURCE_CATEGORY cat, + List sds, + boolean useSelectedExtensions, + ShExGenerator.ConstraintTranslationPolicy policy) { + if ((sds == null) || (sds.isEmpty())) { + throw new FHIRException("No StructuredDefinition found!"); + } + + ShexGeneratorTestUtils shexTestUtils = new ShexGeneratorTestUtils(); + List sdDefs = shexTestUtils.getSDs(sds, cat); + + printList(cat.toString(), sdDefs); + System.out.println("************************************************************************"); + System.out.println("Processing " + cat); + System.out.println("************************************************************************"); + + sdDefs.forEach((ShexGeneratorTestUtils.resDef resDef) -> { + String name = resDef.url; + if (resDef.url.indexOf("/") != -1) { + String els[] = resDef.url.split("/"); + name = els[els.length - 1]; + } + System.out.println("******************** " + resDef + " *********************"); + doTestThis(name, resDef.url, useSelectedExtensions, policy, true, true); + }); + + System.out.println("************************ END PROCESSING ******************************"); + + System.out.println("************************************************************************"); + List skipped = this.shexGenerator.getExcludedStructureDefinitionUrls(); + System.out.println("Total Items processed: " + sds.size()); + System.out.println("************************************************************************"); + } } \ No newline at end of file