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