001package org.hl7.fhir.r5.utils; 002 003import java.io.FileInputStream; 004import java.io.FileNotFoundException; 005import java.io.FileOutputStream; 006import java.io.IOException; 007import java.io.InputStream; 008import java.util.ArrayList; 009import java.util.HashMap; 010import java.util.List; 011import java.util.Map; 012 013import org.hl7.fhir.exceptions.FHIRException; 014import org.hl7.fhir.r5.formats.IParser.OutputStyle; 015import org.hl7.fhir.r5.formats.JsonParser; 016import org.hl7.fhir.r5.model.CanonicalResource; 017import org.hl7.fhir.r5.model.ConceptMap; 018import org.hl7.fhir.r5.model.ConceptMap.ConceptMapGroupComponent; 019import org.hl7.fhir.r5.model.ConceptMap.OtherElementComponent; 020import org.hl7.fhir.r5.model.ConceptMap.SourceElementComponent; 021import org.hl7.fhir.r5.model.ConceptMap.TargetElementComponent; 022import org.hl7.fhir.r5.model.DateTimeType; 023import org.hl7.fhir.r5.model.Enumerations.ConceptMapRelationship; 024import org.hl7.fhir.r5.model.Enumerations.PublicationStatus; 025import org.hl7.fhir.r5.model.IdType; 026import org.hl7.fhir.r5.model.StringType; 027import org.hl7.fhir.r5.model.StructureMap; 028import org.hl7.fhir.r5.model.StructureMap.StructureMapGroupComponent; 029import org.hl7.fhir.r5.model.StructureMap.StructureMapGroupRuleComponent; 030import org.hl7.fhir.r5.model.StructureMap.StructureMapGroupRuleDependentComponent; 031import org.hl7.fhir.r5.model.StructureMap.StructureMapGroupRuleSourceComponent; 032import org.hl7.fhir.r5.model.StructureMap.StructureMapGroupRuleTargetComponent; 033import org.hl7.fhir.r5.model.StructureMap.StructureMapTransform; 034import org.hl7.fhir.r5.model.UrlType; 035import org.hl7.fhir.r5.utils.structuremap.StructureMapUtilities; 036import org.hl7.fhir.utilities.CSVReader; 037import org.hl7.fhir.utilities.TextFile; 038import org.hl7.fhir.utilities.Utilities; 039 040public class MappingSheetParser { 041 042 public class MappingRow { 043 private String sequence; 044 private String identifier; 045 private String name; 046 private String dataType; 047 private String cardinality; 048 private String condition; 049 private String attribute; 050 private String type; 051 private String minMax; 052 private String dtMapping; 053 private String vocabMapping; 054 private String derived; 055 private String derivedMapping; 056 private String comments; 057 public String getSequence() { 058 return sequence; 059 } 060 public String getIdentifier() { 061 return identifier; 062 } 063 public String getName() { 064 return name; 065 } 066 public String getDataType() { 067 return dataType; 068 } 069 public String getCardinality() { 070 return cardinality; 071 } 072 public int getCardinalityMin() { 073 return Integer.parseInt(cardinality.split("\\.")[0]); 074 } 075 public String getCardinalityMax() { 076 return cardinality.split("\\.")[2]; 077 } 078 public String getCondition() { 079 return condition; 080 } 081 public String getAttribute() { 082 return attribute; 083 } 084 public String getType() { 085 return type; 086 } 087 public String getMinMax() { 088 return minMax; 089 } 090 public String getDtMapping() { 091 return dtMapping; 092 } 093 public String getVocabMapping() { 094 return vocabMapping; 095 } 096 public String getDerived() { 097 return derived; 098 } 099 public String getDerivedMapping() { 100 return derivedMapping; 101 } 102 public String getComments() { 103 return comments; 104 } 105 106 } 107 108 109 private List<MappingRow> rows = new ArrayList<>(); 110 private Map<String, String> metadata = new HashMap<>(); 111 112 public MappingSheetParser() { 113 super(); 114 } 115 116 public void parse(InputStream stream, String name) throws FHIRException, IOException { 117 CSVReader csv = new CSVReader(stream); 118 checkHeaders1(csv, name); 119 checkHeaders2(csv, name); 120 while (csv.line()) { 121 processRow(csv); 122 } 123 } 124 125 private void checkHeaders1(CSVReader csv, String name) throws FHIRException, IOException { 126 csv.readHeaders(); 127 csv.checkColumn(1, "HL7 v2", "Mapping Sheet "+name); 128 csv.checkColumn(6, "Condition (IF True)", "Mapping Sheet "+name); 129 csv.checkColumn(7, "HL7 FHIR", "Mapping Sheet "+name); 130 csv.checkColumn(14, "Comments", "Mapping Sheet "+name); 131 csv.checkColumn(16, "Name", "Mapping Sheet "+name); 132 csv.checkColumn(17, "Value", "Mapping Sheet "+name); 133 } 134 135 private void checkHeaders2(CSVReader csv, String name) throws FHIRException, IOException { 136 csv.readHeaders(); 137 csv.checkColumn(1, "Display Sequence", "Mapping Sheet "+name); 138 csv.checkColumn(2, "Identifier", "Mapping Sheet "+name); 139 csv.checkColumn(3, "Name", "Mapping Sheet "+name); 140 csv.checkColumn(4, "Data Type", "Mapping Sheet "+name); 141 csv.checkColumn(5, "Cardinality", "Mapping Sheet "+name); 142 csv.checkColumn(7, "FHIR Attribute", "Mapping Sheet "+name); 143 csv.checkColumn(8, "Data Type", "Mapping Sheet "+name); 144 csv.checkColumn(9, "Cardinality", "Mapping Sheet "+name); 145 csv.checkColumn(10, "Data Type Mapping", "Mapping Sheet "+name); 146 csv.checkColumn(11, "Vocabulary Mapping\n(IS, ID, CE, CNE, CWE)", "Mapping Sheet "+name); 147 csv.checkColumn(12, "Derived Mapping", "Mapping Sheet "+name); 148 } 149 150 private void processRow(CSVReader csv) { 151 MappingRow mr = new MappingRow(); 152 mr.sequence = csv.value(1); 153 mr.identifier = csv.value(2); 154 mr.name = csv.value(3); 155 mr.dataType = csv.value(4); 156 mr.cardinality = csv.value(5); 157 mr.condition = csv.value(6); 158 mr.attribute = csv.value(7); 159 mr.type = csv.value(8); 160 mr.minMax = csv.value(9); 161 mr.dtMapping = csv.value(10); 162 mr.vocabMapping = csv.value(11); 163 mr.derived = csv.value(12); 164 if (!Utilities.noString(mr.derived)) { 165 String[] s = mr.derived.split("\\="); 166 mr.derived = s[0].trim(); 167 mr.derivedMapping = s[1].trim(); 168 } 169 mr.comments = csv.value(14); 170 rows.add(mr); 171 if (!org.hl7.fhir.utilities.Utilities.noString(csv.value(16))) 172 metadata.put(csv.value(16), csv.value(17)); 173 } 174 175 public List<MappingRow> getRows() { 176 return rows; 177 } 178 179 public ConceptMap getConceptMap() throws FHIRException { 180 ConceptMap map = new ConceptMap(); 181 loadMetadata(map); 182 if (metadata.containsKey("copyright")) 183 map.setCopyright(metadata.get("copyright")); 184 for (MappingRow row : rows) { 185 SourceElementComponent element = map.getGroupFirstRep().addElement(); 186 element.setCode(row.getIdentifier()); 187 element.setId(row.getSequence()); 188 element.setDisplay(row.getName()+" : "+row.getDataType()+" ["+row.getCardinality()+"]"); 189 element.addExtension(ToolingExtensions.EXT_MAPPING_NAME, new StringType(row.getName())); 190 element.addExtension(ToolingExtensions.EXT_MAPPING_TYPE, new StringType(row.getDataType())); 191 element.addExtension(ToolingExtensions.EXT_MAPPING_CARD, new StringType(row.getCardinality())); 192 if ("N/A".equals(row.getAttribute())) 193 element.setNoMap(true); 194 else { 195 element.getTargetFirstRep().setRelationship(ConceptMapRelationship.RELATEDTO); 196 if (row.getCondition() != null) 197 element.getTargetFirstRep().addDependsOn().setAttribute("http://hl7.org/fhirpath").setValue(new StringType(processCondition(row.getCondition()))); 198 element.getTargetFirstRep().setCode(row.getAttribute()); 199 element.getTargetFirstRep().setDisplay(row.getType()+" : ["+row.getMinMax()+"]"); 200 element.getTargetFirstRep().addExtension(ToolingExtensions.EXT_MAPPING_TGTTYPE, new StringType(row.getType())); 201 element.getTargetFirstRep().addExtension(ToolingExtensions.EXT_MAPPING_TGTCARD, new StringType(row.getMinMax())); 202 if (row.getDerived() != null) 203 element.getTargetFirstRep().getProductFirstRep().setAttribute(row.getDerived()).setValue(new StringType(row.getDerivedMapping())); 204 if (row.getComments() != null) 205 element.getTargetFirstRep().setComment(row.getComments()); 206 if (row.getDtMapping() != null) 207 element.getTargetFirstRep().addExtension("http://hl7.org/fhir/StructureDefinition/ConceptMap-type-mapping", new UrlType("todo#"+row.getDtMapping())); 208 if (row.getVocabMapping() != null) 209 element.getTargetFirstRep().addExtension("http://hl7.org/fhir/StructureDefinition/ConceptMap-vocab-mapping", new UrlType("todo#"+row.getVocabMapping())); 210 } 211 } 212 return map; 213 } 214 215 private String processCondition(String condition) { 216 if (condition.startsWith("IF ") && condition.endsWith(" IS VALUED")) 217 return "`"+condition.substring(4, condition.length()-10)+"`.exists()"; 218 if (condition.startsWith("IF ") && condition.endsWith(" DOES NOT EXIST")) 219 return "`"+condition.substring(4, condition.length()-15)+"`.exists()"; 220 throw new Error("not processed yet: "+condition); 221 } 222 223 private void loadMetadata(CanonicalResource mr) throws FHIRException { 224 if (metadata.containsKey("id")) 225 mr.setId(metadata.get("id")); 226 if (metadata.containsKey("url")) 227 mr.setUrl(metadata.get("url")); 228 if (metadata.containsKey("name")) 229 mr.setName(metadata.get("name")); 230 if (metadata.containsKey("title")) 231 mr.setTitle(metadata.get("title")); 232 if (metadata.containsKey("version")) 233 mr.setVersion(metadata.get("version")); 234 if (metadata.containsKey("status")) 235 mr.setStatus(PublicationStatus.fromCode(metadata.get("status"))); 236 if (metadata.containsKey("date")) 237 mr.setDateElement(new DateTimeType(metadata.get("date"))); 238 if (metadata.containsKey("publisher")) 239 mr.setPublisher(metadata.get("publisher")); 240 if (metadata.containsKey("description")) 241 mr.setDescription(metadata.get("description")); 242 } 243 244 public StructureMap getStructureMap() throws FHIRException { 245 StructureMap map = new StructureMap(); 246 loadMetadata(map); 247 if (metadata.containsKey("copyright")) 248 map.setCopyright(metadata.get("copyright")); 249 StructureMapGroupComponent grp = map.addGroup(); 250 for (MappingRow row : rows) { 251 StructureMapGroupRuleComponent rule = grp.addRule(); 252 rule.setName(row.getSequence()); 253 StructureMapGroupRuleSourceComponent src = rule.getSourceFirstRep(); 254 src.setContext("src"); 255 src.setElement(row.getIdentifier()); 256 src.setMin(row.getCardinalityMin()); 257 src.setMax(row.getCardinalityMax()); 258 src.setType(row.getDataType()); 259 src.addExtension(ToolingExtensions.EXT_MAPPING_NAME, new StringType(row.getName())); 260 if (row.getCondition() != null) { 261 src.setCheck(processCondition(row.getCondition())); 262 } 263 StructureMapGroupRuleTargetComponent tgt = rule.getTargetFirstRep(); 264 tgt.setContext("tgt"); 265 tgt.setElement(row.getAttribute()); 266 tgt.addExtension(ToolingExtensions.EXT_MAPPING_TGTTYPE, new StringType(row.getType())); 267 tgt.addExtension(ToolingExtensions.EXT_MAPPING_TGTCARD, new StringType(row.getMinMax())); 268 if (row.getDtMapping() != null) { 269 src.setVariable("s"); 270 tgt.setVariable("t"); 271 tgt.setTransform(StructureMapTransform.CREATE); 272 StructureMapGroupRuleDependentComponent dep = rule.addDependent(); 273 dep.setName(row.getDtMapping()); 274 dep.addParameter().setValue(new IdType("s")); 275 dep.addParameter().setValue(new IdType("t")); 276 } else if (row.getVocabMapping() != null) { 277 tgt.setTransform(StructureMapTransform.TRANSLATE); 278 tgt.addParameter().setValue(new StringType(row.getVocabMapping())); 279 tgt.addParameter().setValue(new IdType("src")); 280 } else { 281 tgt.setTransform(StructureMapTransform.COPY); 282 } 283 rule.setDocumentation(row.getComments()); 284 if (row.getDerived() != null) { 285 tgt = rule.addTarget(); 286 tgt.setContext("tgt"); 287 tgt.setElement(row.getDerived()); 288 tgt.setTransform(StructureMapTransform.COPY); 289 tgt.addParameter().setValue(new StringType(row.getDerivedMapping())); 290 } 291 } 292 return map; 293 } 294 295 public boolean isSheet(ConceptMap cm) { 296 if (cm.getGroup().size() != 1) 297 return false; 298 ConceptMapGroupComponent grp = cm.getGroupFirstRep(); 299 for (SourceElementComponent e : grp.getElement()) { 300 if (!e.hasExtension(ToolingExtensions.EXT_MAPPING_TYPE)) 301 return false; 302 } 303 return true; 304 } 305 306 public String genSheet(ConceptMap cm) throws FHIRException { 307 StringBuilder b = new StringBuilder(); 308 readConceptMap(cm); 309 b.append("<table class=\"grid\">\r\n"); 310 addHeaderRow1(b); 311 addHeaderRow2(b); 312 for (MappingRow row : rows) 313 addRow(b, row); 314 b.append("</table>\r\n"); 315 return b.toString(); 316 } 317 318 private void addRow(StringBuilder b, MappingRow row) { 319 b.append(" <tr>"); 320 b.append("<td>"+Utilities.escapeXml(nn(row.sequence))+"</td>"); 321 b.append("<td>"+Utilities.escapeXml(nn(row.identifier))+"</td>"); 322 b.append("<td>"+Utilities.escapeXml(nn(row.name))+"</td>"); 323 b.append("<td>"+Utilities.escapeXml(nn(row.dataType))+"</td>"); 324 b.append("<td>"+Utilities.escapeXml(nn(row.cardinality))+"</td>"); 325 b.append("<td>"+Utilities.escapeXml(nn(row.condition))+"</td>"); 326 b.append("<td>"+Utilities.escapeXml(nn(row.attribute))+"</td>"); 327 b.append("<td>"+Utilities.escapeXml(nn(row.type))+"</td>"); 328 b.append("<td>"+Utilities.escapeXml(nn(row.minMax))+"</td>"); 329 b.append("<td>"+Utilities.escapeXml(nn(row.dtMapping))+"</td>"); 330 b.append("<td>"+Utilities.escapeXml(nn(row.vocabMapping))+"</td>"); 331 if (row.derived != null) 332 b.append("<td>"+Utilities.escapeXml(nn(row.derived+"="+row.derivedMapping))+"</td>"); 333 else 334 b.append("<td></td>"); 335 b.append("<td>"+Utilities.escapeXml(nn(row.comments))+"</td>"); 336 b.append("</tr>\r\n"); 337 338 } 339 340 private String nn(String s) { 341 return s == null ? "" : s; 342 } 343 344 private void addHeaderRow1(StringBuilder b) { 345 b.append(" <tr>"); 346 b.append("<td colspan=\"5\" style=\"background-color: lightgreen\"><b>v2</b></td>"); 347 b.append("<td colspan=\"1\"><b>Condition</b></td>"); 348 b.append("<td colspan=\"6\" style=\"background-color: orange\"><b>FHIR</b></td>"); 349 b.append("<td colspan=\"1\"><b>Comments</b></td>"); 350 b.append("</tr>\r\n"); 351 } 352 353 private void addHeaderRow2(StringBuilder b) { 354 b.append(" <tr>"); 355 b.append("<td style=\"background-color: lightgreen\"><b>Display Sequence</b></td>"); 356 b.append("<td style=\"background-color: lightgreen\"><b>Identifier</b></td>"); 357 b.append("<td style=\"background-color: lightgreen\"><b>Name</b></td>"); 358 b.append("<td style=\"background-color: lightgreen\"><b>Data Type</b></td>"); 359 b.append("<td style=\"background-color: lightgreen\"><b>Cardinality</b></td>"); 360 b.append("<td><b></b></td>"); 361 b.append("<td style=\"background-color: orange\"><b>FHIR Attribute</b></td>"); 362 b.append("<td style=\"background-color: orange\"><b>Data Type</b></td>"); 363 b.append("<td style=\"background-color: orange\"><b>Cardinality</b></td>"); 364 b.append("<td style=\"background-color: orange\"><b>Data Type Mapping</b></td>"); 365 b.append("<td style=\"background-color: orange\"><b>Vocabulary Mapping</b></td>"); 366 b.append("<td style=\"background-color: orange\"><b>Derived Mapping</b></td>"); 367 b.append("<td><b></b></td>"); 368 b.append("</tr>\r\n"); 369 } 370 371 private void readConceptMap(ConceptMap cm) throws FHIRException { 372 for (ConceptMapGroupComponent g : cm.getGroup()) { 373 for (SourceElementComponent e : g.getElement()) { 374 if (e.hasId() && e.getTarget().size() == 1 && e.hasExtension(ToolingExtensions.EXT_MAPPING_TYPE)) { 375 TargetElementComponent t = e.getTargetFirstRep(); 376 MappingRow row = new MappingRow(); 377 row.sequence = e.getId(); 378 row.identifier = e.getCode(); 379 row.name = e.getExtensionString(ToolingExtensions.EXT_MAPPING_NAME); 380 row.dataType = e.getExtensionString(ToolingExtensions.EXT_MAPPING_TYPE); 381 row.cardinality = e.getExtensionString(ToolingExtensions.EXT_MAPPING_CARD); 382 if (e.getNoMap() == true) { 383 row.attribute = "N/A"; 384 } else { 385 OtherElementComponent dep = getDependency(t, "http://hl7.org/fhirpath"); 386 if (dep != null) 387 row.condition = dep.getValue().primitiveValue(); 388 row.attribute = t.getCode(); 389 row.type = t.getExtensionString(ToolingExtensions.EXT_MAPPING_TGTTYPE); 390 row.minMax = t.getExtensionString(ToolingExtensions.EXT_MAPPING_TGTCARD); 391 row.dtMapping = t.getExtensionString("http://hl7.org/fhir/StructureDefinition/ConceptMap-type-mapping"); 392 row.vocabMapping = t.getExtensionString("http://hl7.org/fhir/StructureDefinition/ConceptMap-vocab-mapping"); 393 if (t.getProduct().size() > 0) { 394 row.derived = t.getProductFirstRep().getAttribute(); 395 row.derivedMapping = t.getProductFirstRep().getValue().primitiveValue(); 396 } 397 } 398 row.comments = t.getComment(); 399 rows.add(row); 400 } 401 } 402 } 403 } 404 405 406 private OtherElementComponent getDependency(TargetElementComponent t, String prop) { 407 for (OtherElementComponent dep : t.getDependsOn()) { 408 if (prop.equals(dep.getAttribute())) 409 return dep; 410 } 411 return null; 412 } 413 414 private static final String PFX = "<html><link rel=\"stylesheet\" href=\"file:c:\\work\\org.hl7.fhir\\build\\publish\\fhir.css\"/></head><body>\r\n"; 415 private static final String SFX = "<body></html>"; 416 public static void main(String[] args) throws FileNotFoundException, IOException, FHIRException { 417 MappingSheetParser parser = new MappingSheetParser(); 418 parser.parse(new FileInputStream(Utilities.path("[tmp]", "v2-pid.csv")), "v2-pid.csv"); 419 ConceptMap cm = parser.getConceptMap(); 420 StructureMap sm = parser.getStructureMap(); 421 new JsonParser().setOutputStyle(OutputStyle.PRETTY).compose(new FileOutputStream(Utilities.path("[tmp]", "sm.json")), sm); 422 new JsonParser().setOutputStyle(OutputStyle.PRETTY).compose(new FileOutputStream(Utilities.path("[tmp]", "cm.json")), cm); 423 TextFile.stringToFile(StructureMapUtilities.render(sm), Utilities.path("[tmp]", "sm.txt")); 424 TextFile.stringToFile(PFX+parser.genSheet(cm)+SFX, Utilities.path("[tmp]", "map.html")); 425 } 426 427}