001package org.hl7.fhir.r5.renderers.spreadsheets;
002
003import java.io.IOException;
004import java.io.OutputStream;
005
006import org.hl7.fhir.exceptions.DefinitionException;
007import org.hl7.fhir.r5.model.ElementDefinition;
008import org.hl7.fhir.r5.model.StructureDefinition;
009import org.hl7.fhir.utilities.i18n.I18nConstants;
010
011
012import java.io.ByteArrayOutputStream;
013import java.io.IOException;
014import java.io.OutputStream;
015import java.io.UnsupportedEncodingException;
016import java.util.ArrayList;
017import java.util.Enumeration;
018import java.util.HashMap;
019import java.util.List;
020import java.util.Map;
021
022import org.apache.poi.ss.usermodel.BorderStyle;
023import org.apache.poi.ss.usermodel.Cell;
024import org.apache.poi.ss.usermodel.CellStyle;
025import org.apache.poi.ss.usermodel.ConditionalFormattingRule;
026import org.apache.poi.ss.usermodel.FillPatternType;
027import org.apache.poi.ss.usermodel.Font;
028import org.apache.poi.ss.usermodel.FontFormatting;
029import org.apache.poi.ss.usermodel.IndexedColors;
030import org.apache.poi.ss.usermodel.PatternFormatting;
031import org.apache.poi.ss.usermodel.Row;
032import org.apache.poi.ss.usermodel.Sheet;
033import org.apache.poi.ss.usermodel.SheetConditionalFormatting;
034import org.apache.poi.ss.usermodel.VerticalAlignment;
035import org.apache.poi.ss.usermodel.Workbook;
036import org.apache.poi.ss.util.CellAddress;
037import org.apache.poi.ss.util.CellRangeAddress;
038import org.apache.poi.xssf.usermodel.XSSFRow;
039import org.apache.poi.xssf.usermodel.XSSFSheet;
040import org.apache.poi.xssf.usermodel.XSSFWorkbook;
041import org.checkerframework.common.reflection.qual.ForName;
042import org.hl7.fhir.r5.formats.IParser.OutputStyle;
043import org.hl7.fhir.r5.context.IWorkerContext;
044import org.hl7.fhir.r5.context.SimpleWorkerContext;
045import org.hl7.fhir.r5.formats.JsonParser;
046import org.hl7.fhir.r5.formats.XmlParser;
047import org.hl7.fhir.r5.model.CanonicalType;
048import org.hl7.fhir.r5.model.Coding;
049import org.hl7.fhir.r5.model.DataType;
050import org.hl7.fhir.r5.model.ElementDefinition;
051import org.hl7.fhir.r5.model.ElementDefinition.AggregationMode;
052import org.hl7.fhir.r5.model.ElementDefinition.ElementDefinitionConstraintComponent;
053import org.hl7.fhir.r5.model.ElementDefinition.ElementDefinitionMappingComponent;
054import org.hl7.fhir.r5.model.ElementDefinition.ElementDefinitionSlicingDiscriminatorComponent;
055import org.hl7.fhir.r5.model.ElementDefinition.TypeRefComponent;
056import org.hl7.fhir.r5.model.IdType;
057import org.hl7.fhir.r5.model.StringType;
058import org.hl7.fhir.r5.model.StructureDefinition;
059import org.hl7.fhir.r5.model.StructureDefinition.StructureDefinitionContextComponent;
060import org.hl7.fhir.r5.model.StructureDefinition.StructureDefinitionMappingComponent;
061import org.hl7.fhir.r5.model.UriType;
062import org.hl7.fhir.utilities.CommaSeparatedStringBuilder;
063import org.hl7.fhir.utilities.MarkedToMoveToAdjunctPackage;
064import org.hl7.fhir.utilities.TextStreamWriter;
065import org.openxmlformats.schemas.spreadsheetml.x2006.main.CTAutoFilter;
066import org.openxmlformats.schemas.spreadsheetml.x2006.main.CTCustomFilter;
067import org.openxmlformats.schemas.spreadsheetml.x2006.main.CTCustomFilters;
068import org.openxmlformats.schemas.spreadsheetml.x2006.main.CTFilterColumn;
069import org.openxmlformats.schemas.spreadsheetml.x2006.main.CTFilters;
070import org.openxmlformats.schemas.spreadsheetml.x2006.main.STFilterOperator;
071
072
073@MarkedToMoveToAdjunctPackage
074public class StructureDefinitionSpreadsheetGenerator extends CanonicalSpreadsheetGenerator {
075  private XmlParser xml = new XmlParser();
076  private JsonParser json = new JsonParser();
077  private boolean asXml;
078  private boolean hideMustSupportFalse;
079  private List<StructureDefinitionMappingComponent> mapKeys = new ArrayList<StructureDefinitionMappingComponent>();
080
081  private static String[] titles = {
082      "ID", "Path", "Slice Name", "Alias(s)", "Label", "Min", "Max", "Must Support?", "Is Modifier?", "Is Summary?", "Type(s)", "Short", 
083      "Definition", "Comments", "Requirements", "Default Value", "Meaning When Missing", "Fixed Value", "Pattern", "Example",
084      "Minimum Value", "Maximum Value", "Maximum Length", "Binding Strength", "Binding Description", "Binding Value Set", "Code",
085      "Slicing Discriminator", "Slicing Description", "Slicing Ordered", "Slicing Rules", "Base Path", "Base Min", "Base Max",
086      "Condition(s)", "Constraint(s)"};
087
088  public StructureDefinitionSpreadsheetGenerator(IWorkerContext context, boolean valuesAsXml, boolean hideMustSupportFalse) {
089    super(context);
090    this.asXml = valuesAsXml;
091    this.hideMustSupportFalse = hideMustSupportFalse;
092  }
093
094  public StructureDefinitionSpreadsheetGenerator renderStructureDefinition(StructureDefinition sd, boolean forMultiple) throws Exception {
095    if (sd == null) {
096      System.out.println("no structure!");
097    }
098    if (!sd.hasSnapshot()) {
099      throw new DefinitionException(context.formatMessage(I18nConstants.NEEDS_A_SNAPSHOT));
100    }
101    addStructureDefinitionMetadata(renderCanonicalResource(sd, forMultiple), sd);
102    Sheet sheet = forMultiple && hasSheet("Elements") ? getSheet("Elements") : makeSheet("Elements");
103
104    if (sheet.getPhysicalNumberOfRows() == 0) {
105      Row headerRow = sheet.createRow(0);
106      int coffset = forMultiple ? 1 : 0;
107      for (int i = 0; i < titles.length; i++) {
108        if (forMultiple) {
109          addCell(headerRow, 0, "Structure.ID", styles.get("header"));        
110        }
111        addCell(headerRow, i+coffset, titles[i], styles.get("header"));
112      }
113      if (!forMultiple) {
114        int i = titles.length - 1;
115        for (StructureDefinitionMappingComponent map : sd.getMapping()) {
116          i++;
117          addCell(headerRow, i+coffset, "Mapping: " + map.getName(), styles.get("header"));
118        }
119      }
120    }
121
122    for (ElementDefinition child : sd.getSnapshot().getElement()) {
123      processElement(sheet, sd, child, forMultiple);
124    }
125    if (!forMultiple) {
126      configureSheet(sheet, sd);
127    }
128    return this;
129  }
130  
131  public StructureDefinitionSpreadsheetGenerator configure() throws Exception {
132    Sheet sheet = hasSheet("Elements") ? getSheet("Elements") : makeSheet("Elements");
133    configureSheet(sheet, null);
134    return this;
135  }
136
137
138  private void addStructureDefinitionMetadata(Sheet sheet, StructureDefinition sd) {
139    for (Coding k : sd.getKeyword()) {
140      addMetadataRow(sheet, "Keyword", dr.displayDataType(k));
141    }
142    addMetadataRow(sheet, "FHIR Version", sd.getFhirVersionElement().asStringValue());
143    addMetadataRow(sheet, "Kind", sd.getKindElement().asStringValue());
144    addMetadataRow(sheet, "Type", sd.getType());
145    addMetadataRow(sheet, "Base Definition", sd.getBaseDefinition());
146    addMetadataRow(sheet, "Abstract", sd.getAbstractElement().asStringValue());
147    addMetadataRow(sheet, "Derivation", sd.getDerivationElement().asStringValue());
148
149    for (StructureDefinitionContextComponent k : sd.getContext()) {
150      addMetadataRow(sheet, "Context", k.getTypeElement().asStringValue()+":"+k.getExpression());
151    }
152    for (StringType k : sd.getContextInvariant()) {
153      addMetadataRow(sheet, "Context Inv.", k.getValue());
154    }
155    
156  }
157
158  public void processElement(Sheet sheet, StructureDefinition sd, ElementDefinition ed, boolean forMultiple) throws Exception {
159    Row row = sheet.createRow(sheet.getLastRowNum()+1);
160    int i = 0;
161    if (forMultiple) {
162      addCell(row, i++, sd.getId(), styles.get("body"));
163    }
164    addCell(row, i++, ed.getId(), styles.get("body"));
165    addCell(row, i++, ed.getPath());
166    addCell(row, i++, ed.getSliceName());
167    addCell(row, i++, itemList(ed.getAlias()));
168    addCell(row, i++, ed.getLabel());
169    addCell(row, i++, ed.getMin());
170    addCell(row, i++, ed.getMax());
171    addCell(row, i++, ed.getMustSupport() ? "Y" : "");
172    addCell(row, i++, ed.getIsModifier() ? "Y" : "");
173    addCell(row, i++, ed.getIsSummary() ? "Y" : "");
174    addCell(row, i++, itemList(ed.getType()));
175    addCell(row, i++, ed.getShort());
176    addCell(row, i++, ed.getDefinition());
177    addCell(row, i++, ed.getComment());
178    addCell(row, i++, ed.getRequirements());
179    addCell(row, i++, ed.getDefaultValue()!=null ? renderType(ed.getDefaultValue()) : "");
180    addCell(row, i++, ed.getMeaningWhenMissing());
181    addCell(row, i++, ed.hasFixed() ? renderType(ed.getFixed()) : "");
182    addCell(row, i++, ed.hasPattern() ? renderType(ed.getPattern()) : "");
183    addCell(row, i++, ed.hasExample() ? renderType(ed.getExample().get(0).getValue()) : ""); // todo...?
184    addCell(row, i++, ed.hasMinValue() ? renderType(ed.getMinValue()) : "");
185    addCell(row, i++, ed.hasMaxValue() ? renderType(ed.getMaxValue()) : "");
186    addCell(row, i++, (ed.hasMaxLength() ? Integer.toString(ed.getMaxLength()) : ""));
187    if (ed.hasBinding()) {
188      addCell(row, i++, ed.getBinding().getStrength()!=null ? ed.getBinding().getStrength().toCode() : "");
189      addCell(row, i++, ed.getBinding().getDescription());
190      if (ed.getBinding().getValueSet()==null)
191        addCell(row, i++, "");
192      else
193        addCell(row, i++, ed.getBinding().getValueSet());
194    } else {
195      addCell(row, i++, "");
196      addCell(row, i++, "");
197      addCell(row, i++, "");
198    }
199    addCell(row, i++, itemList(ed.getCode()));
200    if (ed.hasSlicing()) {
201      addCell(row, i++, itemList(ed.getSlicing().getDiscriminator()));
202      addCell(row, i++, ed.getSlicing().getDescription());
203      addCell(row, i++, ed.getSlicing().getOrdered());
204      addCell(row, i++, ed.getSlicing().getRules()!=null ? ed.getSlicing().getRules().toCode() : "");
205    } else {
206      addCell(row, i++, "");
207      addCell(row, i++, "");
208      addCell(row, i++, "");      
209      addCell(row, i++, "");      
210    }
211    if (ed.getBase()!=null) {
212      addCell(row, i++, ed.getBase().getPath());
213      addCell(row, i++, ed.getBase().getMin());
214      addCell(row, i++, ed.getBase().getMax());
215    } else {
216      addCell(row, i++, "");
217      addCell(row, i++, "");
218      addCell(row, i++, "");      
219    }
220    addCell(row, i++, itemList(ed.getCondition()));
221    addCell(row, i++, itemList(ed.getConstraint()));
222    if (!forMultiple) {
223      for (StructureDefinitionMappingComponent mapKey : sd.getMapping()) {
224        String mapString = "";
225        for (ElementDefinitionMappingComponent map : ed.getMapping()) {
226          if (map.getIdentity().equals(mapKey.getIdentity()))
227            mapString = map.getMap();        
228        }
229        addCell(row, i++, mapString);
230      }
231    }
232  }
233
234  private String itemList(List l) {
235    StringBuilder s = new StringBuilder();
236    for (int i =0; i< l.size(); i++) {
237      Object o = l.get(i);
238      String val = "";
239      if (o instanceof StringType) {
240        val = ((StringType)o).getValue();
241      } else if (o instanceof UriType) {
242        val = ((UriType)o).getValue();
243      } else if (o instanceof IdType) {
244        val = ((IdType)o).getValue();
245      } else if (o instanceof Enumeration<?>) {
246        val = o.toString();
247      } else if (o instanceof TypeRefComponent) {
248        TypeRefComponent t = (TypeRefComponent)o;
249        val = t.getWorkingCode();
250        if (val == null)
251          val = "";
252        if (val.startsWith("http://hl7.org/fhir/StructureDefinition/"))
253          val = val.substring(40);
254        if (t.hasTargetProfile()) 
255          val = val+ "(" + canonicalList(t.getTargetProfile()) + ")";
256        if (t.hasProfile())
257          val = val + " {" + canonicalList(t.getProfile()) + "}";
258        if (t.hasAggregation()) 
259          val = val + " <<" + aggList(t.getAggregation()) + ">>";
260      } else if (o instanceof Coding) {
261        Coding t = (Coding)o;
262        val = (t.getSystem()==null ? "" : t.getSystem()) + (t.getCode()==null ? "" : "#" + t.getCode()) + (t.getDisplay()==null ? "" : " (" + t.getDisplay() + ")");
263      } else if (o instanceof ElementDefinitionConstraintComponent) {
264        ElementDefinitionConstraintComponent c = (ElementDefinitionConstraintComponent)o;
265        val = c.getKey() + ":" + c.getHuman() + " {" + c.getExpression() + "}";
266      } else if (o instanceof ElementDefinitionSlicingDiscriminatorComponent) {
267        ElementDefinitionSlicingDiscriminatorComponent c = (ElementDefinitionSlicingDiscriminatorComponent)o;
268        val = c.getType().toCode() + ":" + c.getPath() + "}";
269
270      } else {
271        val = o.toString();
272        val = val.substring(val.indexOf("[")+1);
273        val = val.substring(0, val.indexOf("]"));
274      }
275      s = s.append(val);
276      if (i == 0)
277        s.append("\n");
278    }
279    return s.toString();
280  }
281
282  private String aggList(List<org.hl7.fhir.r5.model.Enumeration<AggregationMode>> list) {
283    CommaSeparatedStringBuilder b = new CommaSeparatedStringBuilder();
284    for (org.hl7.fhir.r5.model.Enumeration<AggregationMode> c : list)
285      b.append(c.getValue().toCode());
286    return b.toString();
287  }
288
289  private String canonicalList(List<CanonicalType> list) {
290    CommaSeparatedStringBuilder b = new CommaSeparatedStringBuilder("|");
291    for (CanonicalType c : list) {
292      String v = c.getValue();
293      if (v.startsWith("http://hl7.org/fhir/StructureDefinition/"))
294        v = v.substring(40);
295      b.append(v);
296    }
297    return b.toString();
298  }
299
300  private String renderType(DataType value) throws Exception {
301    if (value == null)
302      return "";
303    if (value.isPrimitive())
304      return value.primitiveValue();
305
306    String s = null;
307    ByteArrayOutputStream bs = new ByteArrayOutputStream();
308    if (asXml) {
309      xml.setOutputStyle(OutputStyle.PRETTY);
310      xml.compose(bs, "", value);
311      bs.close();
312      s = bs.toString();
313      s = s.substring(s.indexOf("\n")+2);
314    } else {
315      json.setOutputStyle(OutputStyle.PRETTY);
316      json.compose(bs, value, "");
317      bs.close();
318      s = bs.toString();
319    }
320    return s;
321  }
322
323
324  public void configureSheet(Sheet sheet, StructureDefinition sd) throws IOException {
325    for (int i=0; i<34; i++) {
326      sheet.autoSizeColumn(i);
327    }
328    sheet.setColumnHidden(2, true);
329    sheet.setColumnHidden(3, true);
330    sheet.setColumnHidden(30, true);
331    sheet.setColumnHidden(31, true);
332    sheet.setColumnHidden(32, true);
333
334    sheet.setColumnWidth(9, columnPixels(20));
335    sheet.setColumnWidth(11, columnPixels(100));
336    sheet.setColumnWidth(12, columnPixels(100));
337    sheet.setColumnWidth(13, columnPixels(100));
338    sheet.setColumnWidth(15, columnPixels(20));
339    sheet.setColumnWidth(16, columnPixels(20));
340    sheet.setColumnWidth(17, columnPixels(20));
341    sheet.setColumnWidth(18, columnPixels(20));
342    sheet.setColumnWidth(34, columnPixels(100));
343
344    if (sd != null) {
345      int i = titles.length - 1;
346      for (StructureDefinitionMappingComponent map : sd.getMapping()) {
347        i++;
348        sheet.setColumnWidth(i, columnPixels(50));
349        sheet.autoSizeColumn(i);
350        //  sheet.setColumnHidden(i,  true);
351      }    
352    }
353    sheet.createFreezePane(2,1);
354
355    if (hideMustSupportFalse) {
356      SheetConditionalFormatting sheetCF = sheet.getSheetConditionalFormatting();
357      String address = "A2:AI" + Math.max(Integer.valueOf(sheet.getLastRowNum()), 2);
358      CellRangeAddress[] regions = {
359          CellRangeAddress.valueOf(address)
360      };
361
362      ConditionalFormattingRule rule1 = sheetCF.createConditionalFormattingRule("$G2<>\"Y\"");
363      PatternFormatting fill1 = rule1.createPatternFormatting();
364      fill1.setFillBackgroundColor(IndexedColors.GREY_25_PERCENT.index);
365      fill1.setFillPattern(PatternFormatting.SOLID_FOREGROUND);
366
367      ConditionalFormattingRule rule2 = sheetCF.createConditionalFormattingRule("$Q2<>\"\"");
368      FontFormatting font = rule2.createFontFormatting();
369      font.setFontColorIndex(IndexedColors.GREY_25_PERCENT.index);
370      font.setFontStyle(true, false);
371
372      sheetCF.addConditionalFormatting(regions, rule1, rule2);
373
374      sheet.setAutoFilter(new CellRangeAddress(0,sheet.getLastRowNum(), 0, titles.length+(sd == null ? 0 : sd.getMapping().size() - 1)));
375
376
377      XSSFSheet xSheet = (XSSFSheet)sheet;
378
379      CTAutoFilter sheetFilter = xSheet.getCTWorksheet().getAutoFilter();
380      CTFilterColumn filterColumn1 = sheetFilter.addNewFilterColumn();
381      filterColumn1.setColId(6);
382      CTCustomFilters filters = filterColumn1.addNewCustomFilters();
383      CTCustomFilter filter1 = filters.addNewCustomFilter();
384      filter1.setOperator(STFilterOperator.NOT_EQUAL);
385      filter1.setVal(" ");
386
387      CTFilterColumn filterColumn2 = sheetFilter.addNewFilterColumn();
388      filterColumn2.setColId(26);
389      CTFilters filters2 = filterColumn2.addNewFilters();
390      filters2.setBlank(true);
391
392      // We have to apply the filter ourselves by hiding the rows: 
393      for (Row row : sheet) {
394        if (row.getRowNum()>0 && (!row.getCell(6).getStringCellValue().equals("Y") || !row.getCell(26).getStringCellValue().isEmpty())) {
395          ((XSSFRow) row).getCTRow().setHidden(true);
396        }
397      }
398    }
399    if (sheet.getLastRowNum() > 0) {
400      sheet.setActiveCell(new CellAddress(sheet.getRow(1).getCell(0)));
401    }
402  }
403
404  public void dump() {
405    super.dump();
406    xml = null;
407    json = null;
408    mapKeys = null;
409    
410  }
411
412}