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