package org.hl7.fhir.dstu3.utils; // remember group resolution // trace - account for which wasn't transformed in the source import java.io.IOException; import java.util.ArrayList; import java.util.EnumSet; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.UUID; import org.hl7.fhir.dstu3.conformance.ProfileUtilities; import org.hl7.fhir.dstu3.conformance.ProfileUtilities.ProfileKnowledgeProvider; import org.hl7.fhir.dstu3.context.IWorkerContext; import org.hl7.fhir.dstu3.context.IWorkerContext.ValidationResult; import org.hl7.fhir.dstu3.elementmodel.Element; import org.hl7.fhir.dstu3.elementmodel.Property; import org.hl7.fhir.dstu3.model.Base; import org.hl7.fhir.dstu3.model.BooleanType; import org.hl7.fhir.dstu3.model.CodeType; import org.hl7.fhir.dstu3.model.CodeableConcept; import org.hl7.fhir.dstu3.model.Coding; import org.hl7.fhir.dstu3.model.ConceptMap; import org.hl7.fhir.dstu3.model.ConceptMap.ConceptMapGroupComponent; import org.hl7.fhir.dstu3.model.ConceptMap.SourceElementComponent; import org.hl7.fhir.dstu3.model.ConceptMap.TargetElementComponent; import org.hl7.fhir.dstu3.model.Constants; import org.hl7.fhir.dstu3.model.ContactDetail; import org.hl7.fhir.dstu3.model.ContactPoint; import org.hl7.fhir.dstu3.model.DecimalType; import org.hl7.fhir.dstu3.model.ElementDefinition; import org.hl7.fhir.dstu3.model.ElementDefinition.ElementDefinitionMappingComponent; import org.hl7.fhir.dstu3.model.ElementDefinition.TypeRefComponent; import org.hl7.fhir.dstu3.model.Enumeration; import org.hl7.fhir.dstu3.model.Enumerations.ConceptMapEquivalence; import org.hl7.fhir.dstu3.model.Enumerations.PublicationStatus; import org.hl7.fhir.dstu3.model.ExpressionNode; import org.hl7.fhir.dstu3.model.ExpressionNode.CollectionStatus; import org.hl7.fhir.dstu3.model.IdType; import org.hl7.fhir.dstu3.model.IntegerType; import org.hl7.fhir.dstu3.model.Narrative.NarrativeStatus; import org.hl7.fhir.dstu3.model.PrimitiveType; import org.hl7.fhir.dstu3.model.Reference; import org.hl7.fhir.dstu3.model.Resource; import org.hl7.fhir.dstu3.model.ResourceFactory; import org.hl7.fhir.dstu3.model.StringType; import org.hl7.fhir.dstu3.model.StructureDefinition; import org.hl7.fhir.dstu3.model.StructureDefinition.StructureDefinitionMappingComponent; import org.hl7.fhir.dstu3.model.StructureDefinition.TypeDerivationRule; import org.hl7.fhir.dstu3.model.StructureMap; import org.hl7.fhir.dstu3.model.StructureMap.StructureMapContextType; import org.hl7.fhir.dstu3.model.StructureMap.StructureMapGroupComponent; import org.hl7.fhir.dstu3.model.StructureMap.StructureMapGroupInputComponent; import org.hl7.fhir.dstu3.model.StructureMap.StructureMapGroupRuleComponent; import org.hl7.fhir.dstu3.model.StructureMap.StructureMapGroupRuleDependentComponent; import org.hl7.fhir.dstu3.model.StructureMap.StructureMapGroupRuleSourceComponent; import org.hl7.fhir.dstu3.model.StructureMap.StructureMapGroupRuleTargetComponent; import org.hl7.fhir.dstu3.model.StructureMap.StructureMapGroupRuleTargetParameterComponent; import org.hl7.fhir.dstu3.model.StructureMap.StructureMapGroupTypeMode; import org.hl7.fhir.dstu3.model.StructureMap.StructureMapInputMode; import org.hl7.fhir.dstu3.model.StructureMap.StructureMapSourceListMode; import org.hl7.fhir.dstu3.model.StructureMap.StructureMapTargetListMode; import org.hl7.fhir.dstu3.model.StructureMap.StructureMapModelMode; import org.hl7.fhir.dstu3.model.StructureMap.StructureMapStructureComponent; import org.hl7.fhir.dstu3.model.StructureMap.StructureMapTransform; import org.hl7.fhir.dstu3.model.Type; import org.hl7.fhir.dstu3.model.TypeDetails; import org.hl7.fhir.dstu3.model.TypeDetails.ProfiledType; import org.hl7.fhir.dstu3.model.UriType; import org.hl7.fhir.dstu3.model.ValueSet; import org.hl7.fhir.dstu3.model.ValueSet.ValueSetExpansionContainsComponent; import org.hl7.fhir.dstu3.terminologies.ValueSetExpander.ValueSetExpansionOutcome; import org.hl7.fhir.dstu3.utils.FHIRLexer.FHIRLexerException; import org.hl7.fhir.dstu3.utils.FHIRPathEngine.IEvaluationContext; import org.hl7.fhir.exceptions.DefinitionException; import org.hl7.fhir.exceptions.FHIRException; import org.hl7.fhir.exceptions.PathEngineException; import org.hl7.fhir.utilities.CommaSeparatedStringBuilder; import org.hl7.fhir.utilities.TextFile; import org.hl7.fhir.utilities.Utilities; import org.hl7.fhir.utilities.xhtml.NodeType; import org.hl7.fhir.utilities.xhtml.XhtmlNode; /** * Services in this class: * * string render(map) - take a structure and convert it to text * map parse(text) - take a text representation and parse it * getTargetType(map) - return the definition for the type to create to hand in * transform(appInfo, source, map, target) - transform from source to target following the map * analyse(appInfo, map) - generate profiles and other analysis artifacts for the targets of the transform * map generateMapFromMappings(StructureDefinition) - build a mapping from a structure definition with loigcal mappings * * @author Grahame Grieve * */ public class StructureMapUtilities { public class ResolvedGroup { public StructureMapGroupComponent target; public StructureMap targetMap; } public static final String MAP_WHERE_CHECK = "map.where.check"; public static final String MAP_WHERE_EXPRESSION = "map.where.expression"; public static final String MAP_SEARCH_EXPRESSION = "map.search.expression"; public static final String MAP_EXPRESSION = "map.transform.expression"; private static final boolean RENDER_MULTIPLE_TARGETS_ONELINE = true; private static final String AUTO_VAR_NAME = "vvv"; public interface ITransformerServices { // public boolean validateByValueSet(Coding code, String valuesetId); public void log(String message); // log internal progress public Base createType(Object appInfo, String name) throws FHIRException; public Base createResource(Object appInfo, Base res); // an already created resource is provided; this is to identify/store it public Coding translate(Object appInfo, Coding source, String conceptMapUrl) throws FHIRException; // public Coding translate(Coding code) // ValueSet validation operation // Translation operation // Lookup another tree of data // Create an instance tree // Return the correct string format to refer to a tree (input or output) public Base resolveReference(Object appContext, String url); public List<Base> performSearch(Object appContext, String url); } private class FFHIRPathHostServices implements IEvaluationContext{ public Base resolveConstant(Object appContext, String name) throws PathEngineException { Variables vars = (Variables) appContext; Base res = vars.get(VariableMode.INPUT, name); if (res == null) res = vars.get(VariableMode.OUTPUT, name); return res; } @Override public TypeDetails resolveConstantType(Object appContext, String name) throws PathEngineException { if (!(appContext instanceof VariablesForProfiling)) throw new Error("Internal Logic Error (wrong type '"+appContext.getClass().getName()+"' in resolveConstantType)"); VariablesForProfiling vars = (VariablesForProfiling) appContext; VariableForProfiling v = vars.get(null, name); if (v == null) throw new PathEngineException("Unknown variable '"+name+"' from variables "+vars.summary()); return v.property.types; } @Override public boolean log(String argument, List<Base> focus) { throw new Error("Not Implemented Yet"); } @Override public FunctionDetails resolveFunction(String functionName) { return null; // throw new Error("Not Implemented Yet"); } @Override public TypeDetails checkFunction(Object appContext, String functionName, List<TypeDetails> parameters) throws PathEngineException { throw new Error("Not Implemented Yet"); } @Override public List<Base> executeFunction(Object appContext, String functionName, List<List<Base>> parameters) { throw new Error("Not Implemented Yet"); } @Override public Base resolveReference(Object appContext, String url) { if (services == null) return null; return services.resolveReference(appContext, url); } } private IWorkerContext worker; private FHIRPathEngine fpe; private Map<String, StructureMap> library; private ITransformerServices services; private ProfileKnowledgeProvider pkp; private Map<String, Integer> ids = new HashMap<String, Integer>(); public StructureMapUtilities(IWorkerContext worker, Map<String, StructureMap> library, ITransformerServices services, ProfileKnowledgeProvider pkp) { super(); this.worker = worker; this.library = library; this.services = services; this.pkp = pkp; fpe = new FHIRPathEngine(worker); fpe.setHostServices(new FFHIRPathHostServices()); } public StructureMapUtilities(IWorkerContext worker, Map<String, StructureMap> library, ITransformerServices services) { super(); this.worker = worker; this.library = library; this.services = services; fpe = new FHIRPathEngine(worker); fpe.setHostServices(new FFHIRPathHostServices()); } public StructureMapUtilities(IWorkerContext worker, Map<String, StructureMap> library) { super(); this.worker = worker; this.library = library; fpe = new FHIRPathEngine(worker); fpe.setHostServices(new FFHIRPathHostServices()); } public StructureMapUtilities(IWorkerContext worker) { super(); this.worker = worker; fpe = new FHIRPathEngine(worker); fpe.setHostServices(new FFHIRPathHostServices()); } public StructureMapUtilities(IWorkerContext worker, ITransformerServices services) { super(); this.worker = worker; this.library = new HashMap<String, StructureMap>(); for (org.hl7.fhir.dstu3.model.MetadataResource bc : worker.allConformanceResources()) { if (bc instanceof StructureMap) library.put(bc.getUrl(), (StructureMap) bc); } this.services = services; fpe = new FHIRPathEngine(worker); fpe.setHostServices(new FFHIRPathHostServices()); } public static String render(StructureMap map) { StringBuilder b = new StringBuilder(); b.append("map \""); b.append(map.getUrl()); b.append("\" = \""); b.append(Utilities.escapeJava(map.getName())); b.append("\"\r\n\r\n"); renderConceptMaps(b, map); renderUses(b, map); renderImports(b, map); for (StructureMapGroupComponent g : map.getGroup()) renderGroup(b, g); return b.toString(); } private static void renderConceptMaps(StringBuilder b, StructureMap map) { for (Resource r : map.getContained()) { if (r instanceof ConceptMap) { produceConceptMap(b, (ConceptMap) r); } } } private static void produceConceptMap(StringBuilder b, ConceptMap cm) { b.append("conceptmap \""); b.append(cm.getId()); b.append("\" {\r\n"); Map<String, String> prefixesSrc = new HashMap<String, String>(); Map<String, String> prefixesTgt = new HashMap<String, String>(); char prefix = 's'; for (ConceptMapGroupComponent cg : cm.getGroup()) { if (!prefixesSrc.containsKey(cg.getSource())) { prefixesSrc.put(cg.getSource(), String.valueOf(prefix)); b.append(" prefix "); b.append(prefix); b.append(" = \""); b.append(cg.getSource()); b.append("\"\r\n"); prefix++; } if (!prefixesTgt.containsKey(cg.getTarget())) { prefixesTgt.put(cg.getTarget(), String.valueOf(prefix)); b.append(" prefix "); b.append(prefix); b.append(" = \""); b.append(cg.getTarget()); b.append("\"\r\n"); prefix++; } } b.append("\r\n"); for (ConceptMapGroupComponent cg : cm.getGroup()) { for (SourceElementComponent ce : cg.getElement()) { b.append(" "); b.append(prefixesSrc.get(cg.getSource())); b.append(":"); b.append(ce.getCode()); b.append(" "); b.append(getChar(ce.getTargetFirstRep().getEquivalence())); b.append(" "); b.append(prefixesTgt.get(cg.getTarget())); b.append(":"); b.append(ce.getTargetFirstRep().getCode()); b.append("\r\n"); } } b.append("}\r\n\r\n"); } private static Object getChar(ConceptMapEquivalence equivalence) { switch (equivalence) { case RELATEDTO: return "-"; case EQUAL: return "="; case EQUIVALENT: return "=="; case DISJOINT: return "!="; case UNMATCHED: return "--"; case WIDER: return "<="; case SUBSUMES: return "<-"; case NARROWER: return ">="; case SPECIALIZES: return ">-"; case INEXACT: return "~"; default: return "??"; } } private static void renderUses(StringBuilder b, StructureMap map) { for (StructureMapStructureComponent s : map.getStructure()) { b.append("uses \""); b.append(s.getUrl()); b.append("\" "); if (s.hasAlias()) { b.append("alias "); b.append(s.getAlias()); b.append(" "); } b.append("as "); b.append(s.getMode().toCode()); b.append("\r\n"); renderDoco(b, s.getDocumentation()); } if (map.hasStructure()) b.append("\r\n"); } private static void renderImports(StringBuilder b, StructureMap map) { for (UriType s : map.getImport()) { b.append("imports \""); b.append(s.getValue()); b.append("\"\r\n"); } if (map.hasImport()) b.append("\r\n"); } public static String groupToString(StructureMapGroupComponent g) { StringBuilder b = new StringBuilder(); renderGroup(b, g); return b.toString(); } private static void renderGroup(StringBuilder b, StructureMapGroupComponent g) { b.append("group "); switch (g.getTypeMode()) { case TYPES: b.append("for types"); case TYPEANDTYPES: b.append("for type+types "); default: // NONE, NULL } b.append("for types "); b.append(g.getName()); if (g.hasExtends()) { b.append(" extends "); b.append(g.getExtends()); } if (g.hasDocumentation()) renderDoco(b, g.getDocumentation()); b.append("\r\n"); for (StructureMapGroupInputComponent gi : g.getInput()) { b.append(" input "); b.append(gi.getName()); if (gi.hasType()) { b.append(" : "); b.append(gi.getType()); } b.append(" as "); b.append(gi.getMode().toCode()); b.append("\r\n"); } if (g.hasInput()) b.append("\r\n"); for (StructureMapGroupRuleComponent r : g.getRule()) { renderRule(b, r, 2); } b.append("\r\nendgroup\r\n"); } public static String ruleToString(StructureMapGroupRuleComponent r) { StringBuilder b = new StringBuilder(); renderRule(b, r, 0); return b.toString(); } private static void renderRule(StringBuilder b, StructureMapGroupRuleComponent r, int indent) { for (int i = 0; i < indent; i++) b.append(' '); b.append(r.getName()); b.append(" : for "); boolean canBeAbbreviated = checkisSimple(r); boolean first = true; for (StructureMapGroupRuleSourceComponent rs : r.getSource()) { if (first) first = false; else b.append(", "); renderSource(b, rs, canBeAbbreviated); } if (r.getTarget().size() > 1) { b.append(" make "); first = true; for (StructureMapGroupRuleTargetComponent rt : r.getTarget()) { if (first) first = false; else b.append(", "); if (RENDER_MULTIPLE_TARGETS_ONELINE) b.append(' '); else { b.append("\r\n"); for (int i = 0; i < indent+4; i++) b.append(' '); } renderTarget(b, rt, false); } } else if (r.hasTarget()) { b.append(" make "); renderTarget(b, r.getTarget().get(0), canBeAbbreviated); } if (!canBeAbbreviated) { if (r.hasRule()) { b.append(" then {\r\n"); renderDoco(b, r.getDocumentation()); for (StructureMapGroupRuleComponent ir : r.getRule()) { renderRule(b, ir, indent+2); } for (int i = 0; i < indent; i++) b.append(' '); b.append("}\r\n"); } else { if (r.hasDependent()) { b.append(" then "); first = true; for (StructureMapGroupRuleDependentComponent rd : r.getDependent()) { if (first) first = false; else b.append(", "); b.append(rd.getName()); b.append("("); boolean ifirst = true; for (StringType rdp : rd.getVariable()) { if (ifirst) ifirst = false; else b.append(", "); b.append(rdp.asStringValue()); } b.append(")"); } } } } renderDoco(b, r.getDocumentation()); b.append("\r\n"); } private static boolean checkisSimple(StructureMapGroupRuleComponent r) { return (r.getSource().size() == 1 && r.getSourceFirstRep().hasElement() && r.getSourceFirstRep().hasVariable()) && (r.getTarget().size() == 1 && r.getTargetFirstRep().hasVariable() && (r.getTargetFirstRep().getTransform() == null || r.getTargetFirstRep().getTransform() == StructureMapTransform.CREATE) && r.getTargetFirstRep().getParameter().size() == 0) && (r.getDependent().size() == 0); } public static String sourceToString(StructureMapGroupRuleSourceComponent r) { StringBuilder b = new StringBuilder(); renderSource(b, r, false); return b.toString(); } private static void renderSource(StringBuilder b, StructureMapGroupRuleSourceComponent rs, boolean abbreviate) { b.append(rs.getContext()); if (rs.getContext().equals("@search")) { b.append('('); b.append(rs.getElement()); b.append(')'); } else if (rs.hasElement()) { b.append('.'); b.append(rs.getElement()); } if (rs.hasType()) { b.append(" : "); b.append(rs.getType()); if (rs.hasMin()) { b.append(" "); b.append(rs.getMin()); b.append(".."); b.append(rs.getMax()); } } if (rs.hasListMode()) { b.append(" "); b.append(rs.getListMode().toCode()); } if (rs.hasDefaultValue()) { b.append(" default "); assert rs.getDefaultValue() instanceof StringType; b.append("\""+Utilities.escapeJson(((StringType) rs.getDefaultValue()).asStringValue())+"\""); } if (!abbreviate && rs.hasVariable()) { b.append(" as "); b.append(rs.getVariable()); } if (rs.hasCondition()) { b.append(" where "); b.append(rs.getCondition()); } if (rs.hasCheck()) { b.append(" check "); b.append(rs.getCheck()); } } public static String targetToString(StructureMapGroupRuleTargetComponent rt) { StringBuilder b = new StringBuilder(); renderTarget(b, rt, false); return b.toString(); } private static void renderTarget(StringBuilder b, StructureMapGroupRuleTargetComponent rt, boolean abbreviate) { if (rt.hasContext()) { if (rt.getContextType() == StructureMapContextType.TYPE) b.append("@"); b.append(rt.getContext()); if (rt.hasElement()) { b.append('.'); b.append(rt.getElement()); } } if (!abbreviate && rt.hasTransform()) { if (rt.hasContext()) b.append(" = "); if (rt.getTransform() == StructureMapTransform.COPY && rt.getParameter().size() == 1) { renderTransformParam(b, rt.getParameter().get(0)); } else if (rt.getTransform() == StructureMapTransform.EVALUATE && rt.getParameter().size() == 1) { b.append("("); b.append("\""+((StringType) rt.getParameter().get(0).getValue()).asStringValue()+"\""); b.append(")"); } else if (rt.getTransform() == StructureMapTransform.EVALUATE && rt.getParameter().size() == 2) { b.append(rt.getTransform().toCode()); b.append("("); b.append(((IdType) rt.getParameter().get(0).getValue()).asStringValue()); b.append("\""+((StringType) rt.getParameter().get(1).getValue()).asStringValue()+"\""); b.append(")"); } else { b.append(rt.getTransform().toCode()); b.append("("); boolean first = true; for (StructureMapGroupRuleTargetParameterComponent rtp : rt.getParameter()) { if (first) first = false; else b.append(", "); renderTransformParam(b, rtp); } b.append(")"); } } if (!abbreviate && rt.hasVariable()) { b.append(" as "); b.append(rt.getVariable()); } for (Enumeration<StructureMapTargetListMode> lm : rt.getListMode()) { b.append(" "); b.append(lm.getValue().toCode()); if (lm.getValue() == StructureMapTargetListMode.SHARE) { b.append(" "); b.append(rt.getListRuleId()); } } } public static String paramToString(StructureMapGroupRuleTargetParameterComponent rtp) { StringBuilder b = new StringBuilder(); renderTransformParam(b, rtp); return b.toString(); } private static void renderTransformParam(StringBuilder b, StructureMapGroupRuleTargetParameterComponent rtp) { try { if (rtp.hasValueBooleanType()) b.append(rtp.getValueBooleanType().asStringValue()); else if (rtp.hasValueDecimalType()) b.append(rtp.getValueDecimalType().asStringValue()); else if (rtp.hasValueIdType()) b.append(rtp.getValueIdType().asStringValue()); else if (rtp.hasValueDecimalType()) b.append(rtp.getValueDecimalType().asStringValue()); else if (rtp.hasValueIntegerType()) b.append(rtp.getValueIntegerType().asStringValue()); else b.append("\""+Utilities.escapeJava(rtp.getValueStringType().asStringValue())+"\""); } catch (FHIRException e) { e.printStackTrace(); b.append("error!"); } } private static void renderDoco(StringBuilder b, String doco) { if (Utilities.noString(doco)) return; b.append(" // "); b.append(doco.replace("\r\n", " ").replace("\r", " ").replace("\n", " ")); } public StructureMap parse(String text) throws FHIRException { FHIRLexer lexer = new FHIRLexer(text); if (lexer.done()) throw lexer.error("Map Input cannot be empty"); lexer.skipComments(); lexer.token("map"); StructureMap result = new StructureMap(); result.setUrl(lexer.readConstant("url")); lexer.token("="); result.setName(lexer.readConstant("name")); lexer.skipComments(); while (lexer.hasToken("conceptmap")) parseConceptMap(result, lexer); while (lexer.hasToken("uses")) parseUses(result, lexer); while (lexer.hasToken("imports")) parseImports(result, lexer); parseGroup(result, lexer); while (!lexer.done()) { parseGroup(result, lexer); } return result; } private void parseConceptMap(StructureMap result, FHIRLexer lexer) throws FHIRLexerException { lexer.token("conceptmap"); ConceptMap map = new ConceptMap(); String id = lexer.readConstant("map id"); if (!id.startsWith("#")) lexer.error("Concept Map identifier must start with #"); map.setId(id); map.setStatus(PublicationStatus.DRAFT); // todo: how to add this to the text format result.getContained().add(map); lexer.token("{"); lexer.skipComments(); // lexer.token("source"); // map.setSource(new UriType(lexer.readConstant("source"))); // lexer.token("target"); // map.setSource(new UriType(lexer.readConstant("target"))); Map<String, String> prefixes = new HashMap<String, String>(); while (lexer.hasToken("prefix")) { lexer.token("prefix"); String n = lexer.take(); lexer.token("="); String v = lexer.readConstant("prefix url"); prefixes.put(n, v); } while (!lexer.hasToken("}")) { String srcs = readPrefix(prefixes, lexer); lexer.token(":"); String sc = lexer.getCurrent().startsWith("\"") ? lexer.readConstant("code") : lexer.take(); ConceptMapEquivalence eq = readEquivalence(lexer); String tgts = (eq != ConceptMapEquivalence.UNMATCHED) ? readPrefix(prefixes, lexer) : ""; ConceptMapGroupComponent g = getGroup(map, srcs, tgts); SourceElementComponent e = g.addElement(); e.setCode(sc); if (e.getCode().startsWith("\"")) e.setCode(lexer.processConstant(e.getCode())); TargetElementComponent tgt = e.addTarget(); if (eq != ConceptMapEquivalence.EQUIVALENT) tgt.setEquivalence(eq); if (tgt.getEquivalence() != ConceptMapEquivalence.UNMATCHED) { lexer.token(":"); tgt.setCode(lexer.take()); if (tgt.getCode().startsWith("\"")) tgt.setCode(lexer.processConstant(tgt.getCode())); } if (lexer.hasComment()) tgt.setComment(lexer.take().substring(2).trim()); } lexer.token("}"); } private ConceptMapGroupComponent getGroup(ConceptMap map, String srcs, String tgts) { for (ConceptMapGroupComponent grp : map.getGroup()) { if (grp.getSource().equals(srcs)) if ((tgts == null && !grp.hasTarget()) || (tgts != null && tgts.equals(grp.getTarget()))) return grp; } ConceptMapGroupComponent grp = map.addGroup(); grp.setSource(srcs); grp.setTarget(tgts); return grp; } private String readPrefix(Map<String, String> prefixes, FHIRLexer lexer) throws FHIRLexerException { String prefix = lexer.take(); if (!prefixes.containsKey(prefix)) throw lexer.error("Unknown prefix '"+prefix+"'"); return prefixes.get(prefix); } private ConceptMapEquivalence readEquivalence(FHIRLexer lexer) throws FHIRLexerException { String token = lexer.take(); if (token.equals("-")) return ConceptMapEquivalence.RELATEDTO; if (token.equals("=")) return ConceptMapEquivalence.EQUAL; if (token.equals("==")) return ConceptMapEquivalence.EQUIVALENT; if (token.equals("!=")) return ConceptMapEquivalence.DISJOINT; if (token.equals("--")) return ConceptMapEquivalence.UNMATCHED; if (token.equals("<=")) return ConceptMapEquivalence.WIDER; if (token.equals("<-")) return ConceptMapEquivalence.SUBSUMES; if (token.equals(">=")) return ConceptMapEquivalence.NARROWER; if (token.equals(">-")) return ConceptMapEquivalence.SPECIALIZES; if (token.equals("~")) return ConceptMapEquivalence.INEXACT; throw lexer.error("Unknown equivalence token '"+token+"'"); } private void parseUses(StructureMap result, FHIRLexer lexer) throws FHIRException { lexer.token("uses"); StructureMapStructureComponent st = result.addStructure(); st.setUrl(lexer.readConstant("url")); if (lexer.hasToken("alias")) { lexer.token("alias"); st.setAlias(lexer.take()); } lexer.token("as"); st.setMode(StructureMapModelMode.fromCode(lexer.take())); lexer.skipToken(";"); if (lexer.hasComment()) { st.setDocumentation(lexer.take().substring(2).trim()); } lexer.skipComments(); } private void parseImports(StructureMap result, FHIRLexer lexer) throws FHIRException { lexer.token("imports"); result.addImport(lexer.readConstant("url")); lexer.skipToken(";"); if (lexer.hasComment()) { lexer.next(); } lexer.skipComments(); } private void parseGroup(StructureMap result, FHIRLexer lexer) throws FHIRException { lexer.token("group"); StructureMapGroupComponent group = result.addGroup(); if (lexer.hasToken("for")) { lexer.token("for"); if ("type".equals(lexer.getCurrent())) { lexer.token("type"); lexer.token("+"); lexer.token("types"); group.setTypeMode(StructureMapGroupTypeMode.TYPEANDTYPES); } else { lexer.token("types"); group.setTypeMode(StructureMapGroupTypeMode.TYPES); } } else group.setTypeMode(StructureMapGroupTypeMode.NONE); group.setName(lexer.take()); if (lexer.hasToken("extends")) { lexer.next(); group.setExtends(lexer.take()); } lexer.skipComments(); while (lexer.hasToken("input")) parseInput(group, lexer); while (!lexer.hasToken("endgroup")) { if (lexer.done()) throw lexer.error("premature termination expecting 'endgroup'"); parseRule(result, group.getRule(), lexer); } lexer.next(); lexer.skipComments(); } private void parseInput(StructureMapGroupComponent group, FHIRLexer lexer) throws FHIRException { lexer.token("input"); StructureMapGroupInputComponent input = group.addInput(); input.setName(lexer.take()); if (lexer.hasToken(":")) { lexer.token(":"); input.setType(lexer.take()); } lexer.token("as"); input.setMode(StructureMapInputMode.fromCode(lexer.take())); if (lexer.hasComment()) { input.setDocumentation(lexer.take().substring(2).trim()); } lexer.skipToken(";"); lexer.skipComments(); } private void parseRule(StructureMap map, List<StructureMapGroupRuleComponent> list, FHIRLexer lexer) throws FHIRException { StructureMapGroupRuleComponent rule = new StructureMapGroupRuleComponent(); list.add(rule); rule.setName(lexer.takeDottedToken()); lexer.token(":"); lexer.token("for"); boolean done = false; while (!done) { parseSource(rule, lexer); done = !lexer.hasToken(","); if (!done) lexer.next(); } if (lexer.hasToken("make")) { lexer.token("make"); done = false; while (!done) { parseTarget(rule, lexer); done = !lexer.hasToken(","); if (!done) lexer.next(); } } if (lexer.hasToken("then")) { lexer.token("then"); if (lexer.hasToken("{")) { lexer.token("{"); if (lexer.hasComment()) { rule.setDocumentation(lexer.take().substring(2).trim()); } lexer.skipComments(); while (!lexer.hasToken("}")) { if (lexer.done()) throw lexer.error("premature termination expecting '}' in nested group"); parseRule(map, rule.getRule(), lexer); } lexer.token("}"); } else { done = false; while (!done) { parseRuleReference(rule, lexer); done = !lexer.hasToken(","); if (!done) lexer.next(); } } } else if (lexer.hasComment()) { rule.setDocumentation(lexer.take().substring(2).trim()); } if (isSimpleSyntax(rule)) { rule.getSourceFirstRep().setVariable(AUTO_VAR_NAME); rule.getTargetFirstRep().setVariable(AUTO_VAR_NAME); rule.getTargetFirstRep().setTransform(StructureMapTransform.CREATE); // with no parameter - e.g. imply what is to be created // no dependencies - imply what is to be done based on types } lexer.skipComments(); } private boolean isSimpleSyntax(StructureMapGroupRuleComponent rule) { return (rule.getSource().size() == 1 && rule.getSourceFirstRep().hasContext() && rule.getSourceFirstRep().hasElement() && !rule.getSourceFirstRep().hasVariable()) && (rule.getTarget().size() == 1 && rule.getTargetFirstRep().hasContext() && rule.getTargetFirstRep().hasElement() && !rule.getTargetFirstRep().hasVariable() && !rule.getTargetFirstRep().hasParameter()) && (rule.getDependent().size() == 0 && rule.getRule().size() == 0); } private void parseRuleReference(StructureMapGroupRuleComponent rule, FHIRLexer lexer) throws FHIRLexerException { StructureMapGroupRuleDependentComponent ref = rule.addDependent(); ref.setName(lexer.take()); lexer.token("("); boolean done = false; while (!done) { ref.addVariable(lexer.take()); done = !lexer.hasToken(","); if (!done) lexer.next(); } lexer.token(")"); } private void parseSource(StructureMapGroupRuleComponent rule, FHIRLexer lexer) throws FHIRException { StructureMapGroupRuleSourceComponent source = rule.addSource(); source.setContext(lexer.take()); if (source.getContext().equals("search") && lexer.hasToken("(")) { source.setContext("@search"); lexer.take(); ExpressionNode node = fpe.parse(lexer); source.setUserData(MAP_SEARCH_EXPRESSION, node); source.setElement(node.toString()); lexer.token(")"); } else if (lexer.hasToken(".")) { lexer.token("."); source.setElement(lexer.take()); } if (lexer.hasToken(":")) { // type and cardinality lexer.token(":"); source.setType(lexer.takeDottedToken()); if (!lexer.hasToken("as", "first", "last", "not_first", "not_last", "only_one", "default")) { source.setMin(lexer.takeInt()); lexer.token(".."); source.setMax(lexer.take()); } } if (lexer.hasToken("default")) { lexer.token("default"); source.setDefaultValue(new StringType(lexer.readConstant("default value"))); } if (Utilities.existsInList(lexer.getCurrent(), "first", "last", "not_first", "not_last", "only_one")) source.setListMode(StructureMapSourceListMode.fromCode(lexer.take())); if (lexer.hasToken("as")) { lexer.take(); source.setVariable(lexer.take()); } if (lexer.hasToken("where")) { lexer.take(); ExpressionNode node = fpe.parse(lexer); source.setUserData(MAP_WHERE_EXPRESSION, node); source.setCondition(node.toString()); } if (lexer.hasToken("check")) { lexer.take(); ExpressionNode node = fpe.parse(lexer); source.setUserData(MAP_WHERE_CHECK, node); source.setCheck(node.toString()); } } private void parseTarget(StructureMapGroupRuleComponent rule, FHIRLexer lexer) throws FHIRException { StructureMapGroupRuleTargetComponent target = rule.addTarget(); String start = lexer.take(); if (lexer.hasToken(".")) { target.setContext(start); target.setContextType(StructureMapContextType.VARIABLE); start = null; lexer.token("."); target.setElement(lexer.take()); } String name; boolean isConstant = false; if (lexer.hasToken("=")) { if (start != null) target.setContext(start); lexer.token("="); isConstant = lexer.isConstant(true); name = lexer.take(); } else name = start; if ("(".equals(name)) { // inline fluentpath expression target.setTransform(StructureMapTransform.EVALUATE); ExpressionNode node = fpe.parse(lexer); target.setUserData(MAP_EXPRESSION, node); target.addParameter().setValue(new StringType(node.toString())); lexer.token(")"); } else if (lexer.hasToken("(")) { target.setTransform(StructureMapTransform.fromCode(name)); lexer.token("("); if (target.getTransform() == StructureMapTransform.EVALUATE) { parseParameter(target, lexer); lexer.token(","); ExpressionNode node = fpe.parse(lexer); target.setUserData(MAP_EXPRESSION, node); target.addParameter().setValue(new StringType(node.toString())); } else { while (!lexer.hasToken(")")) { parseParameter(target, lexer); if (!lexer.hasToken(")")) lexer.token(","); } } lexer.token(")"); } else if (name != null) { target.setTransform(StructureMapTransform.COPY); if (!isConstant) { String id = name; while (lexer.hasToken(".")) { id = id + lexer.take() + lexer.take(); } target.addParameter().setValue(new IdType(id)); } else target.addParameter().setValue(readConstant(name, lexer)); } if (lexer.hasToken("as")) { lexer.take(); target.setVariable(lexer.take()); } while (Utilities.existsInList(lexer.getCurrent(), "first", "last", "share", "collate")) { if (lexer.getCurrent().equals("share")) { target.addListMode(StructureMapTargetListMode.SHARE); lexer.next(); target.setListRuleId(lexer.take()); } else if (lexer.getCurrent().equals("first")) target.addListMode(StructureMapTargetListMode.FIRST); else target.addListMode(StructureMapTargetListMode.LAST); lexer.next(); } } private void parseParameter(StructureMapGroupRuleTargetComponent target, FHIRLexer lexer) throws FHIRLexerException { if (!lexer.isConstant(true)) { target.addParameter().setValue(new IdType(lexer.take())); } else if (lexer.isStringConstant()) target.addParameter().setValue(new StringType(lexer.readConstant("??"))); else { target.addParameter().setValue(readConstant(lexer.take(), lexer)); } } private Type readConstant(String s, FHIRLexer lexer) throws FHIRLexerException { if (Utilities.isInteger(s)) return new IntegerType(s); else if (Utilities.isDecimal(s)) return new DecimalType(s); else if (Utilities.existsInList(s, "true", "false")) return new BooleanType(s.equals("true")); else return new StringType(lexer.processConstant(s)); } public StructureDefinition getTargetType(StructureMap map) throws FHIRException { boolean found = false; StructureDefinition res = null; for (StructureMapStructureComponent uses : map.getStructure()) { if (uses.getMode() == StructureMapModelMode.TARGET) { if (found) throw new FHIRException("Multiple targets found in map "+map.getUrl()); found = true; res = worker.fetchResource(StructureDefinition.class, uses.getUrl()); if (res == null) throw new FHIRException("Unable to find "+uses.getUrl()+" referenced from map "+map.getUrl()); } } if (res == null) throw new FHIRException("No targets found in map "+map.getUrl()); return res; } public enum VariableMode { INPUT, OUTPUT } public class Variable { private VariableMode mode; private String name; private Base object; public Variable(VariableMode mode, String name, Base object) { super(); this.mode = mode; this.name = name; this.object = object; } public VariableMode getMode() { return mode; } public String getName() { return name; } public Base getObject() { return object; } public String summary() { return name+": "+object.fhirType(); } } public class Variables { private List<Variable> list = new ArrayList<Variable>(); public void add(VariableMode mode, String name, Base object) { Variable vv = null; for (Variable v : list) if ((v.mode == mode) && v.getName().equals(name)) vv = v; if (vv != null) list.remove(vv); list.add(new Variable(mode, name, object)); } public Variables copy() { Variables result = new Variables(); result.list.addAll(list); return result; } public Base get(VariableMode mode, String name) { for (Variable v : list) if ((v.mode == mode) && v.getName().equals(name)) return v.getObject(); return null; } public String summary() { CommaSeparatedStringBuilder s = new CommaSeparatedStringBuilder(); CommaSeparatedStringBuilder t = new CommaSeparatedStringBuilder(); for (Variable v : list) if (v.mode == VariableMode.INPUT) s.append(v.summary()); else t.append(v.summary()); return "source variables ["+s.toString()+"], target variables ["+t.toString()+"]"; } } public class TransformContext { private Object appInfo; public TransformContext(Object appInfo) { super(); this.appInfo = appInfo; } public Object getAppInfo() { return appInfo; } } private void log(String cnt) { if (services != null) services.log(cnt); } /** * Given an item, return all the children that conform to the pattern described in name * * Possible patterns: * - a simple name (which may be the base of a name with [] e.g. value[x]) * - a name with a type replacement e.g. valueCodeableConcept * - * which means all children * - ** which means all descendents * * @param item * @param name * @param result * @throws FHIRException */ protected void getChildrenByName(Base item, String name, List<Base> result) throws FHIRException { for (Base v : item.listChildrenByName(name, true)) if (v != null) result.add(v); } public void transform(Object appInfo, Base source, StructureMap map, Base target) throws FHIRException { TransformContext context = new TransformContext(appInfo); log("Start Transform "+map.getUrl()); StructureMapGroupComponent g = map.getGroup().get(0); Variables vars = new Variables(); vars.add(VariableMode.INPUT, getInputName(g, StructureMapInputMode.SOURCE, "source"), source); vars.add(VariableMode.OUTPUT, getInputName(g, StructureMapInputMode.TARGET, "target"), target); executeGroup("", context, map, vars, g); if (target instanceof Element) ((Element) target).sort(); } private String getInputName(StructureMapGroupComponent g, StructureMapInputMode mode, String def) throws DefinitionException { String name = null; for (StructureMapGroupInputComponent inp : g.getInput()) { if (inp.getMode() == mode) if (name != null) throw new DefinitionException("This engine does not support multiple source inputs"); else name = inp.getName(); } return name == null ? def : name; } private void executeGroup(String indent, TransformContext context, StructureMap map, Variables vars, StructureMapGroupComponent group) throws FHIRException { log(indent+"Group : "+group.getName()); // todo: check inputs if (group.hasExtends()) { ResolvedGroup rg = resolveGroupReference(map, group, group.getExtends()); executeGroup(indent+" ", context, rg.targetMap, vars, rg.target); } for (StructureMapGroupRuleComponent r : group.getRule()) { executeRule(indent+" ", context, map, vars, group, r); } } private void executeRule(String indent, TransformContext context, StructureMap map, Variables vars, StructureMapGroupComponent group, StructureMapGroupRuleComponent rule) throws FHIRException { log(indent+"rule : "+rule.getName()); if (rule.getName().contains("CarePlan.participant-unlink")) System.out.println("debug"); Variables srcVars = vars.copy(); if (rule.getSource().size() != 1) throw new FHIRException("Rule \""+rule.getName()+"\": not handled yet"); List<Variables> source = processSource(rule.getName(), context, srcVars, rule.getSource().get(0)); if (source != null) { for (Variables v : source) { for (StructureMapGroupRuleTargetComponent t : rule.getTarget()) { processTarget(rule.getName(), context, v, map, group, t, rule.getSource().size() == 1 ? rule.getSourceFirstRep().getVariable() : null); } if (rule.hasRule()) { for (StructureMapGroupRuleComponent childrule : rule.getRule()) { executeRule(indent +" ", context, map, v, group, childrule); } } else if (rule.hasDependent()) { for (StructureMapGroupRuleDependentComponent dependent : rule.getDependent()) { executeDependency(indent+" ", context, map, v, group, dependent); } } else if (rule.getSource().size() == 1 && rule.getSourceFirstRep().hasVariable() && rule.getTarget().size() == 1 && rule.getTargetFirstRep().hasVariable() && rule.getTargetFirstRep().getTransform() == StructureMapTransform.CREATE && !rule.getTargetFirstRep().hasParameter()) { // simple inferred, map by type Base src = v.get(VariableMode.INPUT, rule.getSourceFirstRep().getVariable()); Base tgt = v.get(VariableMode.OUTPUT, rule.getTargetFirstRep().getVariable()); String srcType = src.fhirType(); String tgtType = tgt.fhirType(); ResolvedGroup defGroup = resolveGroupByTypes(map, rule.getName(), group, srcType, tgtType); Variables vdef = new Variables(); vdef.add(VariableMode.INPUT, defGroup.target.getInput().get(0).getName(), src); vdef.add(VariableMode.OUTPUT, defGroup.target.getInput().get(1).getName(), tgt); executeGroup(indent+" ", context, defGroup.targetMap, vdef, defGroup.target); } } } } private void executeDependency(String indent, TransformContext context, StructureMap map, Variables vin, StructureMapGroupComponent group, StructureMapGroupRuleDependentComponent dependent) throws FHIRException { ResolvedGroup rg = resolveGroupReference(map, group, dependent.getName()); if (rg.target.getInput().size() != dependent.getVariable().size()) { throw new FHIRException("Rule '"+dependent.getName()+"' has "+Integer.toString(rg.target.getInput().size())+" but the invocation has "+Integer.toString(dependent.getVariable().size())+" variables"); } Variables v = new Variables(); for (int i = 0; i < rg.target.getInput().size(); i++) { StructureMapGroupInputComponent input = rg.target.getInput().get(i); StringType rdp = dependent.getVariable().get(i); String var = rdp.asStringValue(); VariableMode mode = input.getMode() == StructureMapInputMode.SOURCE ? VariableMode.INPUT : VariableMode.OUTPUT; Base vv = vin.get(mode, var); if (vv == null && mode == VariableMode.INPUT) //* once source, always source. but target can be treated as source at user convenient vv = vin.get(VariableMode.OUTPUT, var); if (vv == null) throw new FHIRException("Rule '"+dependent.getName()+"' "+mode.toString()+" variable '"+input.getName()+"' named as '"+var+"' has no value"); v.add(mode, input.getName(), vv); } executeGroup(indent+" ", context, rg.targetMap, v, rg.target); } private String determineTypeFromSourceType(StructureMap map, StructureMapGroupComponent source, Base base, String[] types) throws FHIRException { String type = base.fhirType(); String kn = "type^"+type; if (source.hasUserData(kn)) return source.getUserString(kn); ResolvedGroup res = new ResolvedGroup(); res.targetMap = null; res.target = null; for (StructureMapGroupComponent grp : map.getGroup()) { if (matchesByType(map, grp, type)) { if (res.targetMap == null) { res.targetMap = map; res.target = grp; } else throw new FHIRException("Multiple possible matches looking for default rule for '"+type+"'"); } } if (res.targetMap != null) { String result = getActualType(res.targetMap, res.target.getInput().get(1).getType()); source.setUserData(kn, result); return result; } for (UriType imp : map.getImport()) { List<StructureMap> impMapList = findMatchingMaps(imp.getValue()); if (impMapList.size() == 0) throw new FHIRException("Unable to find map(s) for "+imp.getValue()); for (StructureMap impMap : impMapList) { if (!impMap.getUrl().equals(map.getUrl())) { for (StructureMapGroupComponent grp : impMap.getGroup()) { if (matchesByType(impMap, grp, type)) { if (res.targetMap == null) { res.targetMap = impMap; res.target = grp; } else throw new FHIRException("Multiple possible matches for default rule for '"+type+"' in "+res.targetMap.getUrl()+" ("+res.target.getName()+") and "+impMap.getUrl()+" ("+grp.getName()+")"); } } } } } if (res.target == null) throw new FHIRException("No matches found for default rule for '"+type+"' from "+map.getUrl()); String result = getActualType(res.targetMap, res.target.getInput().get(1).getType()); // should be .getType, but R2... source.setUserData(kn, result); return result; } private List<StructureMap> findMatchingMaps(String value) { List<StructureMap> res = new ArrayList<StructureMap>(); if (value.contains("*")) { for (StructureMap sm : library.values()) { if (urlMatches(value, sm.getUrl())) { res.add(sm); } } } else { StructureMap sm = library.get(value); if (sm != null) res.add(sm); } Set<String> check = new HashSet<String>(); for (StructureMap sm : res) { if (check.contains(sm.getUrl())) throw new Error("duplicate"); else check.add(sm.getUrl()); } return res; } private boolean urlMatches(String mask, String url) { return url.length() > mask.length() && url.startsWith(mask.substring(0, mask.indexOf("*"))) && url.endsWith(mask.substring(mask.indexOf("*")+1)) ; } private ResolvedGroup resolveGroupByTypes(StructureMap map, String ruleid, StructureMapGroupComponent source, String srcType, String tgtType) throws FHIRException { String kn = "types^"+srcType+":"+tgtType; if (source.hasUserData(kn)) return (ResolvedGroup) source.getUserData(kn); ResolvedGroup res = new ResolvedGroup(); res.targetMap = null; res.target = null; for (StructureMapGroupComponent grp : map.getGroup()) { if (matchesByType(map, grp, srcType, tgtType)) { if (res.targetMap == null) { res.targetMap = map; res.target = grp; } else throw new FHIRException("Multiple possible matches looking for rule for '"+srcType+"/"+tgtType+"', from rule '"+ruleid+"'"); } } if (res.targetMap != null) { source.setUserData(kn, res); return res; } for (UriType imp : map.getImport()) { List<StructureMap> impMapList = findMatchingMaps(imp.getValue()); if (impMapList.size() == 0) throw new FHIRException("Unable to find map(s) for "+imp.getValue()); for (StructureMap impMap : impMapList) { if (!impMap.getUrl().equals(map.getUrl())) { for (StructureMapGroupComponent grp : impMap.getGroup()) { if (matchesByType(impMap, grp, srcType, tgtType)) { if (res.targetMap == null) { res.targetMap = impMap; res.target = grp; } else throw new FHIRException("Multiple possible matches for rule for '"+srcType+"/"+tgtType+"' in "+res.targetMap.getUrl()+" and "+impMap.getUrl()+", from rule '"+ruleid+"'"); } } } } } if (res.target == null) throw new FHIRException("No matches found for rule for '"+srcType+"/"+tgtType+"' from "+map.getUrl()+", from rule '"+ruleid+"'"); source.setUserData(kn, res); return res; } private boolean matchesByType(StructureMap map, StructureMapGroupComponent grp, String type) throws FHIRException { if (grp.getTypeMode() != StructureMapGroupTypeMode.TYPEANDTYPES) return false; if (grp.getInput().size() != 2 || grp.getInput().get(0).getMode() != StructureMapInputMode.SOURCE || grp.getInput().get(1).getMode() != StructureMapInputMode.TARGET) return false; return matchesType(map, type, grp.getInput().get(0).getType()); } private boolean matchesByType(StructureMap map, StructureMapGroupComponent grp, String srcType, String tgtType) throws FHIRException { if (grp.getTypeMode() == StructureMapGroupTypeMode.NONE) return false; if (grp.getInput().size() != 2 || grp.getInput().get(0).getMode() != StructureMapInputMode.SOURCE || grp.getInput().get(1).getMode() != StructureMapInputMode.TARGET) return false; if (!grp.getInput().get(0).hasType() || !grp.getInput().get(1).hasType()) return false; return matchesType(map, srcType, grp.getInput().get(0).getType()) && matchesType(map, tgtType, grp.getInput().get(1).getType()); } private boolean matchesType(StructureMap map, String actualType, String statedType) throws FHIRException { // check the aliases for (StructureMapStructureComponent imp : map.getStructure()) { if (imp.hasAlias() && statedType.equals(imp.getAlias())) { StructureDefinition sd = worker.fetchResource(StructureDefinition.class, imp.getUrl()); if (sd != null) statedType = sd.getType(); break; } } return actualType.equals(statedType); } private String getActualType(StructureMap map, String statedType) throws FHIRException { // check the aliases for (StructureMapStructureComponent imp : map.getStructure()) { if (imp.hasAlias() && statedType.equals(imp.getAlias())) { StructureDefinition sd = worker.fetchResource(StructureDefinition.class, imp.getUrl()); if (sd == null) throw new FHIRException("Unable to resolve structure "+imp.getUrl()); return sd.getId(); // should be sd.getType(), but R2... } } return statedType; } private ResolvedGroup resolveGroupReference(StructureMap map, StructureMapGroupComponent source, String name) throws FHIRException { String kn = "ref^"+name; if (source.hasUserData(kn)) return (ResolvedGroup) source.getUserData(kn); ResolvedGroup res = new ResolvedGroup(); res.targetMap = null; res.target = null; for (StructureMapGroupComponent grp : map.getGroup()) { if (grp.getName().equals(name)) { if (res.targetMap == null) { res.targetMap = map; res.target = grp; } else throw new FHIRException("Multiple possible matches for rule '"+name+"'"); } } if (res.targetMap != null) { source.setUserData(kn, res); return res; } for (UriType imp : map.getImport()) { List<StructureMap> impMapList = findMatchingMaps(imp.getValue()); if (impMapList.size() == 0) throw new FHIRException("Unable to find map(s) for "+imp.getValue()); for (StructureMap impMap : impMapList) { if (!impMap.getUrl().equals(map.getUrl())) { for (StructureMapGroupComponent grp : impMap.getGroup()) { if (grp.getName().equals(name)) { if (res.targetMap == null) { res.targetMap = impMap; res.target = grp; } else throw new FHIRException("Multiple possible matches for rule '"+name+"' in "+res.targetMap.getUrl()+" and "+impMap.getUrl()); } } } } } if (res.target == null) throw new FHIRException("No matches found for rule '"+name+"'. Reference found in "+map.getUrl()); source.setUserData(kn, res); return res; } private List<Variables> processSource(String ruleId, TransformContext context, Variables vars, StructureMapGroupRuleSourceComponent src) throws FHIRException { List<Base> items; if (src.getContext().equals("@search")) { ExpressionNode expr = (ExpressionNode) src.getUserData(MAP_SEARCH_EXPRESSION); if (expr == null) { expr = fpe.parse(src.getElement()); src.setUserData(MAP_SEARCH_EXPRESSION, expr); } String search = fpe.evaluateToString(vars, null, new StringType(), expr); // string is a holder of nothing to ensure that variables are processed correctly items = services.performSearch(context.appInfo, search); } else { items = new ArrayList<Base>(); Base b = vars.get(VariableMode.INPUT, src.getContext()); if (b == null) throw new FHIRException("Unknown input variable "+src.getContext()); if (!src.hasElement()) items.add(b); else { getChildrenByName(b, src.getElement(), items); if (items.size() == 0 && src.hasDefaultValue()) items.add(src.getDefaultValue()); } } if (src.hasType()) { List<Base> remove = new ArrayList<Base>(); for (Base item : items) { if (item != null && !isType(item, src.getType())) { remove.add(item); } } items.removeAll(remove); } if (src.hasCondition()) { ExpressionNode expr = (ExpressionNode) src.getUserData(MAP_WHERE_EXPRESSION); if (expr == null) { expr = fpe.parse(src.getCondition()); // fpe.check(context.appInfo, ??, ??, expr) src.setUserData(MAP_WHERE_EXPRESSION, expr); } List<Base> remove = new ArrayList<Base>(); for (Base item : items) { if (!fpe.evaluateToBoolean(vars, null, item, expr)) remove.add(item); } items.removeAll(remove); } if (src.hasCheck()) { ExpressionNode expr = (ExpressionNode) src.getUserData(MAP_WHERE_CHECK); if (expr == null) { expr = fpe.parse(src.getCheck()); // fpe.check(context.appInfo, ??, ??, expr) src.setUserData(MAP_WHERE_CHECK, expr); } List<Base> remove = new ArrayList<Base>(); for (Base item : items) { if (!fpe.evaluateToBoolean(vars, null, item, expr)) throw new FHIRException("Rule \""+ruleId+"\": Check condition failed"); } } if (src.hasListMode() && !items.isEmpty()) { switch (src.getListMode()) { case FIRST: Base bt = items.get(0); items.clear(); items.add(bt); break; case NOTFIRST: if (items.size() > 0) items.remove(0); break; case LAST: bt = items.get(items.size()-1); items.clear(); items.add(bt); break; case NOTLAST: if (items.size() > 0) items.remove(items.size()-1); break; case ONLYONE: if (items.size() > 1) throw new FHIRException("Rule \""+ruleId+"\": Check condition failed: the collection has more than one item"); break; case NULL: } } List<Variables> result = new ArrayList<Variables>(); for (Base r : items) { Variables v = vars.copy(); if (src.hasVariable()) v.add(VariableMode.INPUT, src.getVariable(), r); result.add(v); } return result; } private boolean isType(Base item, String type) { if (type.equals(item.fhirType())) return true; return false; } private void processTarget(String ruleId, TransformContext context, Variables vars, StructureMap map, StructureMapGroupComponent group, StructureMapGroupRuleTargetComponent tgt, String srcVar) throws FHIRException { Base dest = null; if (tgt.hasContext()) { dest = vars.get(VariableMode.OUTPUT, tgt.getContext()); if (dest == null) throw new FHIRException("Rule \""+ruleId+"\": target context not known: "+tgt.getContext()); if (!tgt.hasElement()) throw new FHIRException("Rule \""+ruleId+"\": Not supported yet"); } Base v = null; if (tgt.hasTransform()) { v = runTransform(ruleId, context, map, group, tgt, vars, dest, tgt.getElement(), srcVar); if (v != null && dest != null) v = dest.setProperty(tgt.getElement().hashCode(), tgt.getElement(), v); // reset v because some implementations may have to rewrite v when setting the value } else if (dest != null) v = dest.makeProperty(tgt.getElement().hashCode(), tgt.getElement()); if (tgt.hasVariable() && v != null) vars.add(VariableMode.OUTPUT, tgt.getVariable(), v); } private Base runTransform(String ruleId, TransformContext context, StructureMap map, StructureMapGroupComponent group, StructureMapGroupRuleTargetComponent tgt, Variables vars, Base dest, String element, String srcVar) throws FHIRException { try { switch (tgt.getTransform()) { case CREATE : String tn; if (tgt.getParameter().isEmpty()) { // we have to work out the type. First, we see if there is a single type for the target. If there is, we use that String[] types = dest.getTypesForProperty(element.hashCode(), element); if (types.length == 1 && !"*".equals(types[0]) && !types[0].equals("Resource")) tn = types[0]; else if (srcVar != null) { tn = determineTypeFromSourceType(map, group, vars.get(VariableMode.INPUT, srcVar), types); } else throw new Error("Cannot determine type implicitly because there is no single input variable"); } else tn = getParamStringNoNull(vars, tgt.getParameter().get(0), tgt.toString()); Base res = services != null ? services.createType(context.getAppInfo(), tn) : ResourceFactory.createResourceOrType(tn); if (res.isResource() && !res.fhirType().equals("Parameters")) { // res.setIdBase(tgt.getParameter().size() > 1 ? getParamString(vars, tgt.getParameter().get(0)) : UUID.randomUUID().toString().toLowerCase()); if (services != null) res = services.createResource(context.getAppInfo(), res); } if (tgt.hasUserData("profile")) res.setUserData("profile", tgt.getUserData("profile")); return res; case COPY : return getParam(vars, tgt.getParameter().get(0)); case EVALUATE : ExpressionNode expr = (ExpressionNode) tgt.getUserData(MAP_EXPRESSION); if (expr == null) { expr = fpe.parse(getParamStringNoNull(vars, tgt.getParameter().get(1), tgt.toString())); tgt.setUserData(MAP_WHERE_EXPRESSION, expr); } List<Base> v = fpe.evaluate(vars, null, tgt.getParameter().size() == 2 ? getParam(vars, tgt.getParameter().get(0)) : new BooleanType(false), expr); if (v.size() == 0) return null; else if (v.size() != 1) throw new FHIRException("Rule \""+ruleId+"\": Evaluation of "+expr.toString()+" returned "+Integer.toString(v.size())+" objects"); else return v.get(0); case TRUNCATE : String src = getParamString(vars, tgt.getParameter().get(0)); String len = getParamStringNoNull(vars, tgt.getParameter().get(1), tgt.toString()); if (Utilities.isInteger(len)) { int l = Integer.parseInt(len); if (src.length() > l) src = src.substring(0, l); } return new StringType(src); case ESCAPE : throw new Error("Rule \""+ruleId+"\": Transform "+tgt.getTransform().toCode()+" not supported yet"); case CAST : throw new Error("Rule \""+ruleId+"\": Transform "+tgt.getTransform().toCode()+" not supported yet"); case APPEND : throw new Error("Rule \""+ruleId+"\": Transform "+tgt.getTransform().toCode()+" not supported yet"); case TRANSLATE : return translate(context, map, vars, tgt.getParameter()); case REFERENCE : Base b = getParam(vars, tgt.getParameter().get(0)); if (b == null) throw new FHIRException("Rule \""+ruleId+"\": Unable to find parameter "+((IdType) tgt.getParameter().get(0).getValue()).asStringValue()); if (!b.isResource()) throw new FHIRException("Rule \""+ruleId+"\": Transform engine cannot point at an element of type "+b.fhirType()); else { String id = b.getIdBase(); if (id == null) { id = UUID.randomUUID().toString().toLowerCase(); b.setIdBase(id); } return new Reference().setReference(b.fhirType()+"/"+id); } case DATEOP : throw new Error("Rule \""+ruleId+"\": Transform "+tgt.getTransform().toCode()+" not supported yet"); case UUID : return new IdType(UUID.randomUUID().toString()); case POINTER : b = getParam(vars, tgt.getParameter().get(0)); if (b instanceof Resource) return new UriType("urn:uuid:"+((Resource) b).getId()); else throw new FHIRException("Rule \""+ruleId+"\": Transform engine cannot point at an element of type "+b.fhirType()); case CC: CodeableConcept cc = new CodeableConcept(); cc.addCoding(buildCoding(getParamStringNoNull(vars, tgt.getParameter().get(0), tgt.toString()), getParamStringNoNull(vars, tgt.getParameter().get(1), tgt.toString()))); return cc; case C: Coding c = buildCoding(getParamStringNoNull(vars, tgt.getParameter().get(0), tgt.toString()), getParamStringNoNull(vars, tgt.getParameter().get(1), tgt.toString())); return c; default: throw new Error("Rule \""+ruleId+"\": Transform Unknown: "+tgt.getTransform().toCode()); } } catch (Exception e) { throw new FHIRException("Exception executing transform "+tgt.toString()+" on Rule \""+ruleId+"\": "+e.getMessage(), e); } } private Coding buildCoding(String uri, String code) throws FHIRException { // if we can get this as a valueSet, we will String system = null; String display = null; ValueSet vs = Utilities.noString(uri) ? null : worker.fetchResourceWithException(ValueSet.class, uri); if (vs != null) { ValueSetExpansionOutcome vse = worker.expandVS(vs, true, false); if (vse.getError() != null) throw new FHIRException(vse.getError()); CommaSeparatedStringBuilder b = new CommaSeparatedStringBuilder(); for (ValueSetExpansionContainsComponent t : vse.getValueset().getExpansion().getContains()) { if (t.hasCode()) b.append(t.getCode()); if (code.equals(t.getCode()) && t.hasSystem()) { system = t.getSystem(); display = t.getDisplay(); break; } if (code.equalsIgnoreCase(t.getDisplay()) && t.hasSystem()) { system = t.getSystem(); display = t.getDisplay(); break; } } if (system == null) throw new FHIRException("The code '"+code+"' is not in the value set '"+uri+"' (valid codes: "+b.toString()+"; also checked displays)"); } else system = uri; ValidationResult vr = worker.validateCode(system, code, null); if (vr != null && vr.getDisplay() != null) display = vr.getDisplay(); return new Coding().setSystem(system).setCode(code).setDisplay(display); } private String getParamStringNoNull(Variables vars, StructureMapGroupRuleTargetParameterComponent parameter, String message) throws FHIRException { Base b = getParam(vars, parameter); if (b == null) throw new FHIRException("Unable to find a value for "+parameter.toString()+". Context: "+message); if (!b.hasPrimitiveValue()) throw new FHIRException("Found a value for "+parameter.toString()+", but it has a type of "+b.fhirType()+" and cannot be treated as a string. Context: "+message); return b.primitiveValue(); } private String getParamString(Variables vars, StructureMapGroupRuleTargetParameterComponent parameter) throws DefinitionException { Base b = getParam(vars, parameter); if (b == null || !b.hasPrimitiveValue()) return null; return b.primitiveValue(); } private Base getParam(Variables vars, StructureMapGroupRuleTargetParameterComponent parameter) throws DefinitionException { Type p = parameter.getValue(); if (!(p instanceof IdType)) return p; else { String n = ((IdType) p).asStringValue(); Base b = vars.get(VariableMode.INPUT, n); if (b == null) b = vars.get(VariableMode.OUTPUT, n); if (b == null) throw new DefinitionException("Variable "+n+" not found ("+vars.summary()+")"); return b; } } private Base translate(TransformContext context, StructureMap map, Variables vars, List<StructureMapGroupRuleTargetParameterComponent> parameter) throws FHIRException { Base src = getParam(vars, parameter.get(0)); String id = getParamString(vars, parameter.get(1)); String fld = parameter.size() > 2 ? getParamString(vars, parameter.get(2)) : null; return translate(context, map, src, id, fld); } private class SourceElementComponentWrapper { private ConceptMapGroupComponent group; private SourceElementComponent comp; public SourceElementComponentWrapper(ConceptMapGroupComponent group, SourceElementComponent comp) { super(); this.group = group; this.comp = comp; } } public Base translate(TransformContext context, StructureMap map, Base source, String conceptMapUrl, String fieldToReturn) throws FHIRException { Coding src = new Coding(); if (source.isPrimitive()) { src.setCode(source.primitiveValue()); } else if ("Coding".equals(source.fhirType())) { Base[] b = source.getProperty("system".hashCode(), "system", true); if (b.length == 1) src.setSystem(b[0].primitiveValue()); b = source.getProperty("code".hashCode(), "code", true); if (b.length == 1) src.setCode(b[0].primitiveValue()); } else if ("CE".equals(source.fhirType())) { Base[] b = source.getProperty("codeSystem".hashCode(), "codeSystem", true); if (b.length == 1) src.setSystem(b[0].primitiveValue()); b = source.getProperty("code".hashCode(), "code", true); if (b.length == 1) src.setCode(b[0].primitiveValue()); } else throw new FHIRException("Unable to translate source "+source.fhirType()); String su = conceptMapUrl; if (conceptMapUrl.equals("http://hl7.org/fhir/ConceptMap/special-oid2uri")) { String uri = worker.oid2Uri(src.getCode()); if (uri == null) uri = "urn:oid:"+src.getCode(); if ("uri".equals(fieldToReturn)) return new UriType(uri); else throw new FHIRException("Error in return code"); } else { ConceptMap cmap = null; if (conceptMapUrl.startsWith("#")) { for (Resource r : map.getContained()) { if (r instanceof ConceptMap && ((ConceptMap) r).getId().equals(conceptMapUrl.substring(1))) { cmap = (ConceptMap) r; su = map.getUrl()+conceptMapUrl; } } if (cmap == null) throw new FHIRException("Unable to translate - cannot find map "+conceptMapUrl); } else cmap = worker.fetchResource(ConceptMap.class, conceptMapUrl); Coding outcome = null; boolean done = false; String message = null; if (cmap == null) { if (services == null) message = "No map found for "+conceptMapUrl; else { outcome = services.translate(context.appInfo, src, conceptMapUrl); done = true; } } else { List<SourceElementComponentWrapper> list = new ArrayList<SourceElementComponentWrapper>(); for (ConceptMapGroupComponent g : cmap.getGroup()) { for (SourceElementComponent e : g.getElement()) { if (!src.hasSystem() && src.getCode().equals(e.getCode())) list.add(new SourceElementComponentWrapper(g, e)); else if (src.hasSystem() && src.getSystem().equals(g.getSource()) && src.getCode().equals(e.getCode())) list.add(new SourceElementComponentWrapper(g, e)); } } if (list.size() == 0) done = true; else if (list.get(0).comp.getTarget().size() == 0) message = "Concept map "+su+" found no translation for "+src.getCode(); else { for (TargetElementComponent tgt : list.get(0).comp.getTarget()) { if (tgt.getEquivalence() == null || EnumSet.of( ConceptMapEquivalence.EQUAL , ConceptMapEquivalence.RELATEDTO , ConceptMapEquivalence.EQUIVALENT, ConceptMapEquivalence.WIDER).contains(tgt.getEquivalence())) { if (done) { message = "Concept map "+su+" found multiple matches for "+src.getCode(); done = false; } else { done = true; outcome = new Coding().setCode(tgt.getCode()).setSystem(list.get(0).group.getTarget()); } } else if (tgt.getEquivalence() == ConceptMapEquivalence.UNMATCHED) { done = true; } } if (!done) message = "Concept map "+su+" found no usable translation for "+src.getCode(); } } if (!done) throw new FHIRException(message); if (outcome == null) return null; if ("code".equals(fieldToReturn)) return new CodeType(outcome.getCode()); else return outcome; } } public Map<String, StructureMap> getLibrary() { return library; } public class PropertyWithType { private String path; private Property baseProperty; private Property profileProperty; private TypeDetails types; public PropertyWithType(String path, Property baseProperty, Property profileProperty, TypeDetails types) { super(); this.baseProperty = baseProperty; this.profileProperty = profileProperty; this.path = path; this.types = types; } public TypeDetails getTypes() { return types; } public String getPath() { return path; } public Property getBaseProperty() { return baseProperty; } public void setBaseProperty(Property baseProperty) { this.baseProperty = baseProperty; } public Property getProfileProperty() { return profileProperty; } public void setProfileProperty(Property profileProperty) { this.profileProperty = profileProperty; } public String summary() { return path; } } public class VariableForProfiling { private VariableMode mode; private String name; private PropertyWithType property; public VariableForProfiling(VariableMode mode, String name, PropertyWithType property) { super(); this.mode = mode; this.name = name; this.property = property; } public VariableMode getMode() { return mode; } public String getName() { return name; } public PropertyWithType getProperty() { return property; } public String summary() { return name+": "+property.summary(); } } public class VariablesForProfiling { private List<VariableForProfiling> list = new ArrayList<VariableForProfiling>(); private boolean optional; private boolean repeating; public VariablesForProfiling(boolean optional, boolean repeating) { this.optional = optional; this.repeating = repeating; } public void add(VariableMode mode, String name, String path, Property property, TypeDetails types) { add(mode, name, new PropertyWithType(path, property, null, types)); } public void add(VariableMode mode, String name, String path, Property baseProperty, Property profileProperty, TypeDetails types) { add(mode, name, new PropertyWithType(path, baseProperty, profileProperty, types)); } public void add(VariableMode mode, String name, PropertyWithType property) { VariableForProfiling vv = null; for (VariableForProfiling v : list) if ((v.mode == mode) && v.getName().equals(name)) vv = v; if (vv != null) list.remove(vv); list.add(new VariableForProfiling(mode, name, property)); } public VariablesForProfiling copy(boolean optional, boolean repeating) { VariablesForProfiling result = new VariablesForProfiling(optional, repeating); result.list.addAll(list); return result; } public VariablesForProfiling copy() { VariablesForProfiling result = new VariablesForProfiling(optional, repeating); result.list.addAll(list); return result; } public VariableForProfiling get(VariableMode mode, String name) { if (mode == null) { for (VariableForProfiling v : list) if ((v.mode == VariableMode.OUTPUT) && v.getName().equals(name)) return v; for (VariableForProfiling v : list) if ((v.mode == VariableMode.INPUT) && v.getName().equals(name)) return v; } for (VariableForProfiling v : list) if ((v.mode == mode) && v.getName().equals(name)) return v; return null; } public String summary() { CommaSeparatedStringBuilder s = new CommaSeparatedStringBuilder(); CommaSeparatedStringBuilder t = new CommaSeparatedStringBuilder(); for (VariableForProfiling v : list) if (v.mode == VariableMode.INPUT) s.append(v.summary()); else t.append(v.summary()); return "source variables ["+s.toString()+"], target variables ["+t.toString()+"]"; } } public class StructureMapAnalysis { private List<StructureDefinition> profiles = new ArrayList<StructureDefinition>(); private XhtmlNode summary; public List<StructureDefinition> getProfiles() { return profiles; } public XhtmlNode getSummary() { return summary; } } /** * Given a structure map, return a set of analyses on it. * * Returned: * - a list or profiles for what it will create. First profile is the target * - a table with a summary (in xhtml) for easy human undertanding of the mapping * * * @param appInfo * @param map * @return * @throws Exception */ public StructureMapAnalysis analyse(Object appInfo, StructureMap map) throws Exception { ids.clear(); StructureMapAnalysis result = new StructureMapAnalysis(); TransformContext context = new TransformContext(appInfo); VariablesForProfiling vars = new VariablesForProfiling(false, false); StructureMapGroupComponent start = map.getGroup().get(0); for (StructureMapGroupInputComponent t : start.getInput()) { PropertyWithType ti = resolveType(map, t.getType(), t.getMode()); if (t.getMode() == StructureMapInputMode.SOURCE) vars.add(VariableMode.INPUT, t.getName(), ti); else vars.add(VariableMode.OUTPUT, t.getName(), createProfile(map, result.profiles, ti, start.getName(), start)); } result.summary = new XhtmlNode(NodeType.Element, "table").setAttribute("class", "grid"); XhtmlNode tr = result.summary.addTag("tr"); tr.addTag("td").addTag("b").addText("Source"); tr.addTag("td").addTag("b").addText("Target"); log("Start Profiling Transform "+map.getUrl()); analyseGroup("", context, map, vars, start, result); ProfileUtilities pu = new ProfileUtilities(worker, null, pkp); for (StructureDefinition sd : result.getProfiles()) pu.cleanUpDifferential(sd); return result; } private void analyseGroup(String indent, TransformContext context, StructureMap map, VariablesForProfiling vars, StructureMapGroupComponent group, StructureMapAnalysis result) throws Exception { log(indent+"Analyse Group : "+group.getName()); // todo: extends // todo: check inputs XhtmlNode tr = result.summary.addTag("tr").setAttribute("class", "diff-title"); XhtmlNode xs = tr.addTag("td"); XhtmlNode xt = tr.addTag("td"); for (StructureMapGroupInputComponent inp : group.getInput()) { if (inp.getMode() == StructureMapInputMode.SOURCE) noteInput(vars, inp, VariableMode.INPUT, xs); if (inp.getMode() == StructureMapInputMode.TARGET) noteInput(vars, inp, VariableMode.OUTPUT, xt); } for (StructureMapGroupRuleComponent r : group.getRule()) { analyseRule(indent+" ", context, map, vars, group, r, result); } } private void noteInput(VariablesForProfiling vars, StructureMapGroupInputComponent inp, VariableMode mode, XhtmlNode xs) { VariableForProfiling v = vars.get(mode, inp.getName()); if (v != null) xs.addText("Input: "+v.property.getPath()); } private void analyseRule(String indent, TransformContext context, StructureMap map, VariablesForProfiling vars, StructureMapGroupComponent group, StructureMapGroupRuleComponent rule, StructureMapAnalysis result) throws Exception { log(indent+"Analyse rule : "+rule.getName()); XhtmlNode tr = result.summary.addTag("tr"); XhtmlNode xs = tr.addTag("td"); XhtmlNode xt = tr.addTag("td"); VariablesForProfiling srcVars = vars.copy(); if (rule.getSource().size() != 1) throw new Exception("Rule \""+rule.getName()+"\": not handled yet"); VariablesForProfiling source = analyseSource(rule.getName(), context, srcVars, rule.getSourceFirstRep(), xs); TargetWriter tw = new TargetWriter(); for (StructureMapGroupRuleTargetComponent t : rule.getTarget()) { analyseTarget(rule.getName(), context, source, map, t, rule.getSourceFirstRep().getVariable(), tw, result.profiles, rule.getName()); } tw.commit(xt); for (StructureMapGroupRuleComponent childrule : rule.getRule()) { analyseRule(indent+" ", context, map, source, group, childrule, result); } // for (StructureMapGroupRuleDependentComponent dependent : rule.getDependent()) { // executeDependency(indent+" ", context, map, v, group, dependent); // do we need group here? // } } public class StringPair { private String var; private String desc; public StringPair(String var, String desc) { super(); this.var = var; this.desc = desc; } public String getVar() { return var; } public String getDesc() { return desc; } } public class TargetWriter { private Map<String, String> newResources = new HashMap<String, String>(); private List<StringPair> assignments = new ArrayList<StringPair>(); private List<StringPair> keyProps = new ArrayList<StringPair>(); private CommaSeparatedStringBuilder txt = new CommaSeparatedStringBuilder(); public void newResource(String var, String name) { newResources.put(var, name); txt.append("new "+name); } public void valueAssignment(String context, String desc) { assignments.add(new StringPair(context, desc)); txt.append(desc); } public void keyAssignment(String context, String desc) { keyProps.add(new StringPair(context, desc)); txt.append(desc); } public void commit(XhtmlNode xt) { if (newResources.size() == 1 && assignments.size() == 1 && newResources.containsKey(assignments.get(0).getVar()) && keyProps.size() == 1 && newResources.containsKey(keyProps.get(0).getVar()) ) { xt.addText("new "+assignments.get(0).desc+" ("+keyProps.get(0).desc.substring(keyProps.get(0).desc.indexOf(".")+1)+")"); } else if (newResources.size() == 1 && assignments.size() == 1 && newResources.containsKey(assignments.get(0).getVar()) && keyProps.size() == 0) { xt.addText("new "+assignments.get(0).desc); } else { xt.addText(txt.toString()); } } } private VariablesForProfiling analyseSource(String ruleId, TransformContext context, VariablesForProfiling vars, StructureMapGroupRuleSourceComponent src, XhtmlNode td) throws Exception { VariableForProfiling var = vars.get(VariableMode.INPUT, src.getContext()); if (var == null) throw new FHIRException("Rule \""+ruleId+"\": Unknown input variable "+src.getContext()); PropertyWithType prop = var.getProperty(); boolean optional = false; boolean repeating = false; if (src.hasCondition()) { optional = true; } if (src.hasElement()) { Property element = prop.getBaseProperty().getChild(prop.types.getType(), src.getElement()); if (element == null) throw new Exception("Rule \""+ruleId+"\": Unknown element name "+src.getElement()); if (element.getDefinition().getMin() == 0) optional = true; if (element.getDefinition().getMax().equals("*")) repeating = true; VariablesForProfiling result = vars.copy(optional, repeating); TypeDetails type = new TypeDetails(CollectionStatus.SINGLETON); for (TypeRefComponent tr : element.getDefinition().getType()) { if (!tr.hasCode()) throw new Error("Rule \""+ruleId+"\": Element has no type"); ProfiledType pt = new ProfiledType(tr.getCode()); if (tr.hasProfile()) pt.addProfile(tr.getProfile()); if (element.getDefinition().hasBinding()) pt.addBinding(element.getDefinition().getBinding()); type.addType(pt); } td.addText(prop.getPath()+"."+src.getElement()); if (src.hasVariable()) result.add(VariableMode.INPUT, src.getVariable(), new PropertyWithType(prop.getPath()+"."+src.getElement(), element, null, type)); return result; } else { td.addText(prop.getPath()); // ditto! return vars.copy(optional, repeating); } } private void analyseTarget(String ruleId, TransformContext context, VariablesForProfiling vars, StructureMap map, StructureMapGroupRuleTargetComponent tgt, String tv, TargetWriter tw, List<StructureDefinition> profiles, String sliceName) throws Exception { VariableForProfiling var = null; if (tgt.hasContext()) { var = vars.get(VariableMode.OUTPUT, tgt.getContext()); if (var == null) throw new Exception("Rule \""+ruleId+"\": target context not known: "+tgt.getContext()); if (!tgt.hasElement()) throw new Exception("Rule \""+ruleId+"\": Not supported yet"); } TypeDetails type = null; if (tgt.hasTransform()) { type = analyseTransform(context, map, tgt, var, vars); // profiling: dest.setProperty(tgt.getElement().hashCode(), tgt.getElement(), v); } else { Property vp = var.property.baseProperty.getChild(tgt.getElement(), tgt.getElement()); if (vp == null) throw new Exception("Unknown Property "+tgt.getElement()+" on "+var.property.path); type = new TypeDetails(CollectionStatus.SINGLETON, vp.getType(tgt.getElement())); } if (tgt.getTransform() == StructureMapTransform.CREATE) { String s = getParamString(vars, tgt.getParameter().get(0)); if (worker.getResourceNames().contains(s)) tw.newResource(tgt.getVariable(), s); } else { boolean mapsSrc = false; for (StructureMapGroupRuleTargetParameterComponent p : tgt.getParameter()) { Type pr = p.getValue(); if (pr instanceof IdType && ((IdType) pr).asStringValue().equals(tv)) mapsSrc = true; } if (mapsSrc) { if (var == null) throw new Error("Rule \""+ruleId+"\": Attempt to assign with no context"); tw.valueAssignment(tgt.getContext(), var.property.getPath()+"."+tgt.getElement()+getTransformSuffix(tgt.getTransform())); } else if (tgt.hasContext()) { if (isSignificantElement(var.property, tgt.getElement())) { String td = describeTransform(tgt); if (td != null) tw.keyAssignment(tgt.getContext(), var.property.getPath()+"."+tgt.getElement()+" = "+td); } } } Type fixed = generateFixedValue(tgt); PropertyWithType prop = updateProfile(var, tgt.getElement(), type, map, profiles, sliceName, fixed, tgt); if (tgt.hasVariable()) if (tgt.hasElement()) vars.add(VariableMode.OUTPUT, tgt.getVariable(), prop); else vars.add(VariableMode.OUTPUT, tgt.getVariable(), prop); } private Type generateFixedValue(StructureMapGroupRuleTargetComponent tgt) { if (!allParametersFixed(tgt)) return null; if (!tgt.hasTransform()) return null; switch (tgt.getTransform()) { case COPY: return tgt.getParameter().get(0).getValue(); case TRUNCATE: return null; //case ESCAPE: //case CAST: //case APPEND: case TRANSLATE: return null; //case DATEOP, //case UUID, //case POINTER, //case EVALUATE, case CC: CodeableConcept cc = new CodeableConcept(); cc.addCoding(buildCoding(tgt.getParameter().get(0).getValue(), tgt.getParameter().get(1).getValue())); return cc; case C: return buildCoding(tgt.getParameter().get(0).getValue(), tgt.getParameter().get(1).getValue()); case QTY: return null; //case ID, //case CP, default: return null; } } @SuppressWarnings("rawtypes") private Coding buildCoding(Type value1, Type value2) { return new Coding().setSystem(((PrimitiveType) value1).asStringValue()).setCode(((PrimitiveType) value2).asStringValue()) ; } private boolean allParametersFixed(StructureMapGroupRuleTargetComponent tgt) { for (StructureMapGroupRuleTargetParameterComponent p : tgt.getParameter()) { Type pr = p.getValue(); if (pr instanceof IdType) return false; } return true; } private String describeTransform(StructureMapGroupRuleTargetComponent tgt) throws FHIRException { switch (tgt.getTransform()) { case COPY: return null; case TRUNCATE: return null; //case ESCAPE: //case CAST: //case APPEND: case TRANSLATE: return null; //case DATEOP, //case UUID, //case POINTER, //case EVALUATE, case CC: return describeTransformCCorC(tgt); case C: return describeTransformCCorC(tgt); case QTY: return null; //case ID, //case CP, default: return null; } } @SuppressWarnings("rawtypes") private String describeTransformCCorC(StructureMapGroupRuleTargetComponent tgt) throws FHIRException { if (tgt.getParameter().size() < 2) return null; Type p1 = tgt.getParameter().get(0).getValue(); Type p2 = tgt.getParameter().get(1).getValue(); if (p1 instanceof IdType || p2 instanceof IdType) return null; if (!(p1 instanceof PrimitiveType) || !(p2 instanceof PrimitiveType)) return null; String uri = ((PrimitiveType) p1).asStringValue(); String code = ((PrimitiveType) p2).asStringValue(); if (Utilities.noString(uri)) throw new FHIRException("Describe Transform, but the uri is blank"); if (Utilities.noString(code)) throw new FHIRException("Describe Transform, but the code is blank"); Coding c = buildCoding(uri, code); return NarrativeGenerator.describeSystem(c.getSystem())+"#"+c.getCode()+(c.hasDisplay() ? "("+c.getDisplay()+")" : ""); } private boolean isSignificantElement(PropertyWithType property, String element) { if ("Observation".equals(property.getPath())) return "code".equals(element); else if ("Bundle".equals(property.getPath())) return "type".equals(element); else return false; } private String getTransformSuffix(StructureMapTransform transform) { switch (transform) { case COPY: return ""; case TRUNCATE: return " (truncated)"; //case ESCAPE: //case CAST: //case APPEND: case TRANSLATE: return " (translated)"; //case DATEOP, //case UUID, //case POINTER, //case EVALUATE, case CC: return " (--> CodeableConcept)"; case C: return " (--> Coding)"; case QTY: return " (--> Quantity)"; //case ID, //case CP, default: return " {??)"; } } private PropertyWithType updateProfile(VariableForProfiling var, String element, TypeDetails type, StructureMap map, List<StructureDefinition> profiles, String sliceName, Type fixed, StructureMapGroupRuleTargetComponent tgt) throws FHIRException { if (var == null) { assert (Utilities.noString(element)); // 1. start the new structure definition StructureDefinition sdn = worker.fetchResource(StructureDefinition.class, type.getType()); if (sdn == null) throw new FHIRException("Unable to find definition for "+type.getType()); ElementDefinition edn = sdn.getSnapshot().getElementFirstRep(); PropertyWithType pn = createProfile(map, profiles, new PropertyWithType(sdn.getId(), new Property(worker, edn, sdn), null, type), sliceName, tgt); // // 2. hook it into the base bundle // if (type.getType().startsWith("http://hl7.org/fhir/StructureDefinition/") && worker.getResourceNames().contains(type.getType().substring(40))) { // StructureDefinition sd = var.getProperty().profileProperty.getStructure(); // ElementDefinition ed = sd.getDifferential().addElement(); // ed.setPath("Bundle.entry"); // ed.setName(sliceName); // ed.setMax("1"); // well, it is for now... // ed = sd.getDifferential().addElement(); // ed.setPath("Bundle.entry.fullUrl"); // ed.setMin(1); // ed = sd.getDifferential().addElement(); // ed.setPath("Bundle.entry.resource"); // ed.setMin(1); // ed.addType().setCode(pn.getProfileProperty().getStructure().getType()).setProfile(pn.getProfileProperty().getStructure().getUrl()); // } return pn; } else { assert (!Utilities.noString(element)); Property pvb = var.getProperty().getBaseProperty(); Property pvd = var.getProperty().getProfileProperty(); Property pc = pvb.getChild(element, var.property.types); if (pc == null) throw new DefinitionException("Unable to find a definition for "+pvb.getDefinition().getPath()+"."+element); // the profile structure definition (derived) StructureDefinition sd = var.getProperty().profileProperty.getStructure(); ElementDefinition ednew = sd.getDifferential().addElement(); ednew.setPath(var.getProperty().profileProperty.getDefinition().getPath()+"."+pc.getName()); ednew.setUserData("slice-name", sliceName); ednew.setFixed(fixed); for (ProfiledType pt : type.getProfiledTypes()) { if (pt.hasBindings()) ednew.setBinding(pt.getBindings().get(0)); if (pt.getUri().startsWith("http://hl7.org/fhir/StructureDefinition/")) { String t = pt.getUri().substring(40); t = checkType(t, pc, pt.getProfiles()); if (t != null) { if (pt.hasProfiles()) { for (String p : pt.getProfiles()) if (t.equals("Reference")) ednew.addType().setCode(t).setTargetProfile(p); else ednew.addType().setCode(t).setProfile(p); } else ednew.addType().setCode(t); } } } return new PropertyWithType(var.property.path+"."+element, pc, new Property(worker, ednew, sd), type); } } private String checkType(String t, Property pvb, List<String> profiles) throws FHIRException { if (pvb.getDefinition().getType().size() == 1 && isCompatibleType(t, pvb.getDefinition().getType().get(0).getCode()) && profilesMatch(profiles, pvb.getDefinition().getType().get(0).getProfile())) return null; for (TypeRefComponent tr : pvb.getDefinition().getType()) { if (isCompatibleType(t, tr.getCode())) return tr.getCode(); // note what is returned - the base type, not the inferred mapping type } throw new FHIRException("The type "+t+" is not compatible with the allowed types for "+pvb.getDefinition().getPath()); } private boolean profilesMatch(List<String> profiles, String profile) { return profiles == null || profiles.size() == 0 || (profiles.size() == 1 && profiles.get(0).equals(profile)); } private boolean isCompatibleType(String t, String code) { if (t.equals(code)) return true; if (t.equals("string")) { StructureDefinition sd = worker.fetchResource(StructureDefinition.class, "http://hl7.org/fhir/StructureDefinition/"+code); if (sd != null && sd.getBaseDefinition().equals("http://hl7.org/fhir/StructureDefinition/string")) return true; } return false; } private TypeDetails analyseTransform(TransformContext context, StructureMap map, StructureMapGroupRuleTargetComponent tgt, VariableForProfiling var, VariablesForProfiling vars) throws FHIRException { switch (tgt.getTransform()) { case CREATE : String p = getParamString(vars, tgt.getParameter().get(0)); return new TypeDetails(CollectionStatus.SINGLETON, p); case COPY : return getParam(vars, tgt.getParameter().get(0)); case EVALUATE : ExpressionNode expr = (ExpressionNode) tgt.getUserData(MAP_EXPRESSION); if (expr == null) { expr = fpe.parse(getParamString(vars, tgt.getParameter().get(tgt.getParameter().size()-1))); tgt.setUserData(MAP_WHERE_EXPRESSION, expr); } return fpe.check(vars, null, expr); ////case TRUNCATE : //// String src = getParamString(vars, tgt.getParameter().get(0)); //// String len = getParamString(vars, tgt.getParameter().get(1)); //// if (Utilities.isInteger(len)) { //// int l = Integer.parseInt(len); //// if (src.length() > l) //// src = src.substring(0, l); //// } //// return new StringType(src); ////case ESCAPE : //// throw new Error("Transform "+tgt.getTransform().toCode()+" not supported yet"); ////case CAST : //// throw new Error("Transform "+tgt.getTransform().toCode()+" not supported yet"); ////case APPEND : //// throw new Error("Transform "+tgt.getTransform().toCode()+" not supported yet"); case TRANSLATE : return new TypeDetails(CollectionStatus.SINGLETON, "CodeableConcept"); case CC: ProfiledType res = new ProfiledType("CodeableConcept"); if (tgt.getParameter().size() >= 2 && isParamId(vars, tgt.getParameter().get(1))) { TypeDetails td = vars.get(null, getParamId(vars, tgt.getParameter().get(1))).property.types; if (td != null && td.hasBinding()) // todo: do we need to check that there's no implicit translation her? I don't think we do... res.addBinding(td.getBinding()); } return new TypeDetails(CollectionStatus.SINGLETON, res); case C: return new TypeDetails(CollectionStatus.SINGLETON, "Coding"); case QTY: return new TypeDetails(CollectionStatus.SINGLETON, "Quantity"); case REFERENCE : VariableForProfiling vrs = vars.get(VariableMode.OUTPUT, getParamId(vars, tgt.getParameterFirstRep())); if (vrs == null) throw new FHIRException("Unable to resolve variable \""+getParamId(vars, tgt.getParameterFirstRep())+"\""); String profile = vrs.property.getProfileProperty().getStructure().getUrl(); TypeDetails td = new TypeDetails(CollectionStatus.SINGLETON); td.addType("Reference", profile); return td; ////case DATEOP : //// throw new Error("Transform "+tgt.getTransform().toCode()+" not supported yet"); ////case UUID : //// return new IdType(UUID.randomUUID().toString()); ////case POINTER : //// Base b = getParam(vars, tgt.getParameter().get(0)); //// if (b instanceof Resource) //// return new UriType("urn:uuid:"+((Resource) b).getId()); //// else //// throw new FHIRException("Transform engine cannot point at an element of type "+b.fhirType()); default: throw new Error("Transform Unknown or not handled yet: "+tgt.getTransform().toCode()); } } private String getParamString(VariablesForProfiling vars, StructureMapGroupRuleTargetParameterComponent parameter) { Type p = parameter.getValue(); if (p == null || p instanceof IdType) return null; if (!p.hasPrimitiveValue()) return null; return p.primitiveValue(); } private String getParamId(VariablesForProfiling vars, StructureMapGroupRuleTargetParameterComponent parameter) { Type p = parameter.getValue(); if (p == null || !(p instanceof IdType)) return null; return p.primitiveValue(); } private boolean isParamId(VariablesForProfiling vars, StructureMapGroupRuleTargetParameterComponent parameter) { Type p = parameter.getValue(); if (p == null || !(p instanceof IdType)) return false; return vars.get(null, p.primitiveValue()) != null; } private TypeDetails getParam(VariablesForProfiling vars, StructureMapGroupRuleTargetParameterComponent parameter) throws DefinitionException { Type p = parameter.getValue(); if (!(p instanceof IdType)) return new TypeDetails(CollectionStatus.SINGLETON, "http://hl7.org/fhir/StructureDefinition/"+p.fhirType()); else { String n = ((IdType) p).asStringValue(); VariableForProfiling b = vars.get(VariableMode.INPUT, n); if (b == null) b = vars.get(VariableMode.OUTPUT, n); if (b == null) throw new DefinitionException("Variable "+n+" not found ("+vars.summary()+")"); return b.getProperty().getTypes(); } } private PropertyWithType createProfile(StructureMap map, List<StructureDefinition> profiles, PropertyWithType prop, String sliceName, Base ctxt) throws DefinitionException { if (prop.getBaseProperty().getDefinition().getPath().contains(".")) throw new DefinitionException("Unable to process entry point"); String type = prop.getBaseProperty().getDefinition().getPath(); String suffix = ""; if (ids.containsKey(type)) { int id = ids.get(type); id++; ids.put(type, id); suffix = "-"+Integer.toString(id); } else ids.put(type, 0); StructureDefinition profile = new StructureDefinition(); profiles.add(profile); profile.setDerivation(TypeDerivationRule.CONSTRAINT); profile.setType(type); profile.setBaseDefinition(prop.getBaseProperty().getStructure().getUrl()); profile.setName("Profile for "+profile.getType()+" for "+sliceName); profile.setUrl(map.getUrl().replace("StructureMap", "StructureDefinition")+"-"+profile.getType()+suffix); ctxt.setUserData("profile", profile.getUrl()); // then we can easily assign this profile url for validation later when we actually transform profile.setId(map.getId()+"-"+profile.getType()+suffix); profile.setStatus(map.getStatus()); profile.setExperimental(map.getExperimental()); profile.setDescription("Generated automatically from the mapping by the Java Reference Implementation"); for (ContactDetail c : map.getContact()) { ContactDetail p = profile.addContact(); p.setName(c.getName()); for (ContactPoint cc : c.getTelecom()) p.addTelecom(cc); } profile.setDate(map.getDate()); profile.setCopyright(map.getCopyright()); profile.setFhirVersion(Constants.VERSION); profile.setKind(prop.getBaseProperty().getStructure().getKind()); profile.setAbstract(false); ElementDefinition ed = profile.getDifferential().addElement(); ed.setPath(profile.getType()); prop.profileProperty = new Property(worker, ed, profile); return prop; } private PropertyWithType resolveType(StructureMap map, String type, StructureMapInputMode mode) throws Exception { for (StructureMapStructureComponent imp : map.getStructure()) { if ((imp.getMode() == StructureMapModelMode.SOURCE && mode == StructureMapInputMode.SOURCE) || (imp.getMode() == StructureMapModelMode.TARGET && mode == StructureMapInputMode.TARGET)) { StructureDefinition sd = worker.fetchResource(StructureDefinition.class, imp.getUrl()); if (sd == null) throw new Exception("Import "+imp.getUrl()+" cannot be resolved"); if (sd.getId().equals(type)) { return new PropertyWithType(sd.getType(), new Property(worker, sd.getSnapshot().getElement().get(0), sd), null, new TypeDetails(CollectionStatus.SINGLETON, sd.getUrl())); } } } throw new Exception("Unable to find structure definition for "+type+" in imports"); } public StructureMap generateMapFromMappings(StructureDefinition sd) throws IOException, FHIRException { String id = getLogicalMappingId(sd); if (id == null) return null; String prefix = ToolingExtensions.readStringExtension(sd, ToolingExtensions.EXT_MAPPING_PREFIX); String suffix = ToolingExtensions.readStringExtension(sd, ToolingExtensions.EXT_MAPPING_SUFFIX); if (prefix == null || suffix == null) return null; // we build this by text. Any element that has a mapping, we put it's mappings inside it.... StringBuilder b = new StringBuilder(); b.append(prefix); ElementDefinition root = sd.getSnapshot().getElementFirstRep(); String m = getMapping(root, id); if (m != null) b.append(m+"\r\n"); addChildMappings(b, id, "", sd, root, false); b.append("\r\n"); b.append(suffix); b.append("\r\n"); TextFile.stringToFile(b.toString(), "c:\\temp\\test.map"); StructureMap map = parse(b.toString()); map.setId(tail(map.getUrl())); if (!map.hasStatus()) map.setStatus(PublicationStatus.DRAFT); map.getText().setStatus(NarrativeStatus.GENERATED); map.getText().setDiv(new XhtmlNode(NodeType.Element, "div")); map.getText().getDiv().addTag("pre").addText(render(map)); return map; } private String tail(String url) { return url.substring(url.lastIndexOf("/")+1); } private void addChildMappings(StringBuilder b, String id, String indent, StructureDefinition sd, ElementDefinition ed, boolean inner) throws DefinitionException { boolean first = true; List<ElementDefinition> children = ProfileUtilities.getChildMap(sd, ed); for (ElementDefinition child : children) { if (first && inner) { b.append(" then {\r\n"); first = false; } String map = getMapping(child, id); if (map != null) { b.append(indent+" "+child.getPath()+": "+map); addChildMappings(b, id, indent+" ", sd, child, true); b.append("\r\n"); } } if (!first && inner) b.append(indent+"}"); } private String getMapping(ElementDefinition ed, String id) { for (ElementDefinitionMappingComponent map : ed.getMapping()) if (id.equals(map.getIdentity())) return map.getMap(); return null; } private String getLogicalMappingId(StructureDefinition sd) { String id = null; for (StructureDefinitionMappingComponent map : sd.getMapping()) { if ("http://hl7.org/fhir/logical".equals(map.getUri())) return map.getIdentity(); } return null; } }