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