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