001package org.hl7.fhir.r4.utils.formats;
002
003/*-
004 * #%L
005 * org.hl7.fhir.r4
006 * %%
007 * Copyright (C) 2014 - 2019 Health Level 7
008 * %%
009 * Licensed under the Apache License, Version 2.0 (the "License");
010 * you may not use this file except in compliance with the License.
011 * You may obtain a copy of the License at
012 * 
013 *      http://www.apache.org/licenses/LICENSE-2.0
014 * 
015 * Unless required by applicable law or agreed to in writing, software
016 * distributed under the License is distributed on an "AS IS" BASIS,
017 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
018 * See the License for the specific language governing permissions and
019 * limitations under the License.
020 * #L%
021 */
022
023
024import java.io.ByteArrayOutputStream;
025import java.io.IOException;
026import java.io.OutputStream;
027import java.io.UnsupportedEncodingException;
028import java.util.ArrayList;
029import java.util.Enumeration;
030import java.util.HashMap;
031import java.util.List;
032import java.util.Map;
033
034import org.apache.poi.ss.usermodel.BorderStyle;
035import org.apache.poi.ss.usermodel.Cell;
036import org.apache.poi.ss.usermodel.CellStyle;
037import org.apache.poi.ss.usermodel.ConditionalFormattingRule;
038import org.apache.poi.ss.usermodel.FillPatternType;
039import org.apache.poi.ss.usermodel.Font;
040import org.apache.poi.ss.usermodel.FontFormatting;
041import org.apache.poi.ss.usermodel.IndexedColors;
042import org.apache.poi.ss.usermodel.PatternFormatting;
043import org.apache.poi.ss.usermodel.Row;
044import org.apache.poi.ss.usermodel.Sheet;
045import org.apache.poi.ss.usermodel.SheetConditionalFormatting;
046import org.apache.poi.ss.usermodel.VerticalAlignment;
047import org.apache.poi.ss.usermodel.Workbook;
048import org.apache.poi.ss.util.CellAddress;
049import org.apache.poi.ss.util.CellRangeAddress;
050import org.apache.poi.xssf.usermodel.XSSFRow;
051import org.apache.poi.xssf.usermodel.XSSFSheet;
052import org.apache.poi.xssf.usermodel.XSSFWorkbook;
053import org.hl7.fhir.r4.formats.IParser.OutputStyle;
054import org.hl7.fhir.r4.formats.JsonParser;
055import org.hl7.fhir.r4.formats.XmlParser;
056import org.hl7.fhir.r4.model.CanonicalType;
057import org.hl7.fhir.r4.model.Coding;
058import org.hl7.fhir.r4.model.ElementDefinition;
059import org.hl7.fhir.r4.model.ElementDefinition.AggregationMode;
060import org.hl7.fhir.r4.model.ElementDefinition.ElementDefinitionConstraintComponent;
061import org.hl7.fhir.r4.model.ElementDefinition.ElementDefinitionMappingComponent;
062import org.hl7.fhir.r4.model.ElementDefinition.ElementDefinitionSlicingDiscriminatorComponent;
063import org.hl7.fhir.r4.model.ElementDefinition.TypeRefComponent;
064import org.hl7.fhir.r4.model.IdType;
065import org.hl7.fhir.r4.model.StringType;
066import org.hl7.fhir.r4.model.StructureDefinition;
067import org.hl7.fhir.r4.model.StructureDefinition.StructureDefinitionMappingComponent;
068import org.hl7.fhir.r4.model.Type;
069import org.hl7.fhir.r4.model.UriType;
070import org.hl7.fhir.utilities.CommaSeparatedStringBuilder;
071import org.hl7.fhir.utilities.TextStreamWriter;
072import org.openxmlformats.schemas.spreadsheetml.x2006.main.CTAutoFilter;
073import org.openxmlformats.schemas.spreadsheetml.x2006.main.CTCustomFilter;
074import org.openxmlformats.schemas.spreadsheetml.x2006.main.CTCustomFilters;
075import org.openxmlformats.schemas.spreadsheetml.x2006.main.CTFilterColumn;
076import org.openxmlformats.schemas.spreadsheetml.x2006.main.CTFilters;
077import org.openxmlformats.schemas.spreadsheetml.x2006.main.STFilterOperator;
078
079public class XLSXWriter  extends TextStreamWriter  {
080
081  private StructureDefinition def;
082  private List<StructureDefinitionMappingComponent> mapKeys = new ArrayList<StructureDefinitionMappingComponent>();
083  private Map<String, CellStyle> styles;
084  private OutputStream outStream;
085  private XSSFWorkbook wb = new XSSFWorkbook();
086  private Sheet sheet;
087  private XmlParser xml = new XmlParser();
088  private JsonParser json = new JsonParser();
089  private boolean asXml;
090  private boolean hideMustSupportFalse;
091
092  private static String[] titles = {
093      "Path", "Slice Name", "Alias(s)", "Label", "Min", "Max", "Must Support?", "Is Modifier?", "Is Summary?", "Type(s)", "Short", 
094      "Definition", "Comments", "Requirements", "Default Value", "Meaning When Missing", "Fixed Value", "Pattern", "Example",
095      "Minimum Value", "Maximum Value", "Maximum Length", "Binding Strength", "Binding Description", "Binding Value Set", "Code",
096      "Slicing Discriminator", "Slicing Description", "Slicing Ordered", "Slicing Rules", "Base Path", "Base Min", "Base Max",
097      "Condition(s)", "Constraint(s)"};
098
099  public XLSXWriter(OutputStream out, StructureDefinition def, boolean asXml, boolean hideMustSupportFalse) throws UnsupportedEncodingException {
100    super(out);
101    outStream = out;
102    this.asXml = asXml;
103    this.def = def;
104    this.hideMustSupportFalse = hideMustSupportFalse;
105    sheet = wb.createSheet("Elements");
106    styles = createStyles(wb);
107    Row headerRow = sheet.createRow(0);
108    for (int i = 0; i < titles.length; i++) {
109      addCell(headerRow, i, titles[i], styles.get("header"));
110    }
111    int i = titles.length - 1;
112    for (StructureDefinitionMappingComponent map : def.getMapping()) {
113      i++;
114      addCell(headerRow, i, "Mapping: " + map.getName(), styles.get("header"));
115    }    
116  }
117
118  private void addCell(Row row, int pos, String content) {
119    addCell(row, pos, content, styles.get("body"));
120  }
121  
122  public void addCell(Row row, int pos, boolean b) {
123    addCell(row, pos, b ? "Y" : "");
124  }
125
126  public void addCell(Row row, int pos, int content) {
127    addCell(row, pos, Integer.toString(content));
128  }
129  
130  private void addCell(Row row, int pos, String content, CellStyle style) {
131    Cell cell = row.createCell(pos);
132    cell.setCellValue(content);
133    cell.setCellStyle(style);
134  }
135  
136  /**
137   * create a library of cell styles
138   */
139  private static Map<String, CellStyle> createStyles(Workbook wb){
140    Map<String, CellStyle> styles = new HashMap<>();
141
142    CellStyle style;
143    Font headerFont = wb.createFont();
144    headerFont.setBold(true);
145    style = createBorderedStyle(wb);
146    style.setFillForegroundColor(IndexedColors.LIGHT_CORNFLOWER_BLUE.getIndex());
147    style.setFillPattern(FillPatternType.SOLID_FOREGROUND);
148    style.setVerticalAlignment(VerticalAlignment.TOP);
149    style.setWrapText(true);
150    style.setFont(headerFont);
151    styles.put("header", style);
152
153    style = createBorderedStyle(wb);
154    style.setVerticalAlignment(VerticalAlignment.TOP);
155    style.setWrapText(true);    
156    styles.put("body", style);
157
158    return styles;
159  }
160
161  private static CellStyle createBorderedStyle(Workbook wb){
162    BorderStyle thin = BorderStyle.THIN;
163    short black = IndexedColors.GREY_50_PERCENT.getIndex();
164    
165    CellStyle style = wb.createCellStyle();
166    style.setBorderRight(thin);
167    style.setRightBorderColor(black);
168    style.setBorderBottom(thin);
169    style.setBottomBorderColor(black);
170    style.setBorderLeft(thin);
171    style.setLeftBorderColor(black);
172    style.setBorderTop(thin);
173    style.setTopBorderColor(black);
174    return style;
175  }
176  /*  private void findMapKeys(StructureDefinition def, List<StructureDefinitionMappingComponent> maps, IWorkerContext context) {
177        maps.addAll(def.getMapping());
178        if (def.getBaseDefinition()!=null) {
179          StructureDefinition base = context.fetchResource(StructureDefinition.class, def.getBaseDefinition());
180          findMapKeys(base, maps, context);
181        }
182  }*/
183
184  public void processElement(ElementDefinition ed) throws Exception {
185    Row row = sheet.createRow(sheet.getLastRowNum()+1);
186    int i = 0;
187    addCell(row, i++, ed.getPath(), styles.get("body"));
188    addCell(row, i++, ed.getSliceName());
189    addCell(row, i++, itemList(ed.getAlias()));
190    addCell(row, i++, ed.getLabel());
191    addCell(row, i++, ed.getMin());
192    addCell(row, i++, ed.getMax());
193    addCell(row, i++, ed.getMustSupport() ? "Y" : "");
194    addCell(row, i++, ed.getIsModifier() ? "Y" : "");
195    addCell(row, i++, ed.getIsSummary() ? "Y" : "");
196    addCell(row, i++, itemList(ed.getType()));
197    addCell(row, i++, ed.getShort());
198    addCell(row, i++, ed.getDefinition());
199    addCell(row, i++, ed.getComment());
200    addCell(row, i++, ed.getRequirements());
201    addCell(row, i++, ed.getDefaultValue()!=null ? renderType(ed.getDefaultValue()) : "");
202    addCell(row, i++, ed.getMeaningWhenMissing());
203    addCell(row, i++, ed.hasFixed() ? renderType(ed.getFixed()) : "");
204    addCell(row, i++, ed.hasPattern() ? renderType(ed.getPattern()) : "");
205    addCell(row, i++, ed.hasExample() ? renderType(ed.getExample().get(0).getValue()) : ""); // todo...?
206    addCell(row, i++, ed.hasMinValue() ? renderType(ed.getMinValue()) : "");
207    addCell(row, i++, ed.hasMaxValue() ? renderType(ed.getMaxValue()) : "");
208    addCell(row, i++, (ed.hasMaxLength() ? Integer.toString(ed.getMaxLength()) : ""));
209    if (ed.hasBinding()) {
210      addCell(row, i++, ed.getBinding().getStrength()!=null ? ed.getBinding().getStrength().toCode() : "");
211      addCell(row, i++, ed.getBinding().getDescription());
212      if (ed.getBinding().getValueSet()==null)
213        addCell(row, i++, "");
214      else
215        addCell(row, i++, ed.getBinding().getValueSet());
216    } else {
217      addCell(row, i++, "");
218      addCell(row, i++, "");
219      addCell(row, i++, "");
220    }
221    addCell(row, i++, itemList(ed.getCode()));
222    if (ed.hasSlicing()) {
223      addCell(row, i++, itemList(ed.getSlicing().getDiscriminator()));
224      addCell(row, i++, ed.getSlicing().getDescription());
225      addCell(row, i++, ed.getSlicing().getOrdered());
226      addCell(row, i++, ed.getSlicing().getRules()!=null ? ed.getSlicing().getRules().toCode() : "");
227    } else {
228      addCell(row, i++, "");
229      addCell(row, i++, "");
230      addCell(row, i++, "");      
231      addCell(row, i++, "");      
232    }
233    if (ed.getBase()!=null) {
234      addCell(row, i++, ed.getBase().getPath());
235      addCell(row, i++, ed.getBase().getMin());
236      addCell(row, i++, ed.getBase().getMax());
237    } else {
238      addCell(row, i++, "");
239      addCell(row, i++, "");
240      addCell(row, i++, "");      
241    }
242    addCell(row, i++, itemList(ed.getCondition()));
243    addCell(row, i++, itemList(ed.getConstraint()));
244    for (StructureDefinitionMappingComponent mapKey : def.getMapping()) {
245      String mapString = "";
246      for (ElementDefinitionMappingComponent map : ed.getMapping()) {
247        if (map.getIdentity().equals(mapKey.getIdentity()))
248          mapString = map.getMap();        
249      }
250      addCell(row, i++, mapString);
251    }
252  }
253
254
255  private String itemList(List l) {
256    StringBuilder s = new StringBuilder();
257    for (int i =0; i< l.size(); i++) {
258      Object o = l.get(i);
259      String val = "";
260      if (o instanceof StringType) {
261        val = ((StringType)o).getValue();
262      } else if (o instanceof UriType) {
263        val = ((UriType)o).getValue();
264      } else if (o instanceof IdType) {
265        val = ((IdType)o).getValue();
266      } else if (o instanceof Enumeration<?>) {
267        val = o.toString();
268      } else if (o instanceof TypeRefComponent) {
269        TypeRefComponent t = (TypeRefComponent)o;
270          val = t.getWorkingCode();
271          if (val == null)
272            val = "";
273        if (val.startsWith("http://hl7.org/fhir/StructureDefinition/"))
274          val = val.substring(40);
275        if (t.hasTargetProfile()) 
276          val = val+ "(" + canonicalList(t.getTargetProfile()) + ")";
277          if (t.hasProfile())
278            val = val + " {" + canonicalList(t.getProfile()) + "}";
279          if (t.hasAggregation()) 
280          val = val + " <<" + aggList(t.getAggregation()) + ">>";
281      } else if (o instanceof Coding) {
282        Coding t = (Coding)o;
283        val = (t.getSystem()==null ? "" : t.getSystem()) + (t.getCode()==null ? "" : "#" + t.getCode()) + (t.getDisplay()==null ? "" : " (" + t.getDisplay() + ")");
284      } else if (o instanceof ElementDefinitionConstraintComponent) {
285        ElementDefinitionConstraintComponent c = (ElementDefinitionConstraintComponent)o;
286        val = c.getKey() + ":" + c.getHuman() + " {" + c.getExpression() + "}";
287      } else if (o instanceof ElementDefinitionSlicingDiscriminatorComponent) {
288        ElementDefinitionSlicingDiscriminatorComponent c = (ElementDefinitionSlicingDiscriminatorComponent)o;
289        val = c.getType().toCode() + ":" + c.getPath() + "}";
290        
291      } else {
292        val = o.toString();
293        val = val.substring(val.indexOf("[")+1);
294        val = val.substring(0, val.indexOf("]"));
295      }
296      s = s.append(val);
297      if (i == 0)
298        s.append("\n");
299    }
300    return s.toString();
301  }
302  
303  private String aggList(List<org.hl7.fhir.r4.model.Enumeration<AggregationMode>> list) {
304    CommaSeparatedStringBuilder b = new CommaSeparatedStringBuilder();
305    for (org.hl7.fhir.r4.model.Enumeration<AggregationMode> c : list)
306      b.append(c.getValue().toCode());
307    return b.toString();
308  }
309
310  private String canonicalList(List<CanonicalType> list) {
311    CommaSeparatedStringBuilder b = new CommaSeparatedStringBuilder("|");
312    for (CanonicalType c : list) {
313      String v = c.getValue();
314      if (v.startsWith("http://hl7.org/fhir/StructureDefinition/"))
315        v = v.substring(40);
316      b.append(v);
317    }
318    return b.toString();
319  }
320
321  private String renderType(Type value) throws Exception {
322    if (value == null)
323      return "";
324    if (value.isPrimitive())
325      return value.primitiveValue();
326    
327    String s = null;
328    ByteArrayOutputStream bs = new ByteArrayOutputStream();
329    if (asXml) {
330      xml.setOutputStyle(OutputStyle.PRETTY);
331      xml.compose(bs, "", value);
332      bs.close();
333      s = bs.toString();
334      s = s.substring(s.indexOf("\n")+2);
335    } else {
336      json.setOutputStyle(OutputStyle.PRETTY);
337      json.compose(bs, value, "");
338      bs.close();
339      s = bs.toString();
340        }
341    return s;
342  }
343  
344  private int columnPixels(double columns) {
345    double WIDTH_FACTOR = 256;
346    double PADDING = 180;
347    return (int)Math.floor(columns*WIDTH_FACTOR + PADDING);
348  }
349
350  public void dump() throws IOException {
351    for (int i=0; i<34; i++) {
352      sheet.autoSizeColumn(i);
353    }
354      
355    sheet.setColumnHidden(2, true);
356    sheet.setColumnHidden(3, true);
357    sheet.setColumnHidden(30, true);
358    sheet.setColumnHidden(31, true);
359    sheet.setColumnHidden(32, true);
360    
361    sheet.setColumnWidth(9, columnPixels(20));
362    sheet.setColumnWidth(11, columnPixels(100));
363    sheet.setColumnWidth(12, columnPixels(100));
364    sheet.setColumnWidth(13, columnPixels(100));
365    sheet.setColumnWidth(15, columnPixels(20));
366    sheet.setColumnWidth(16, columnPixels(20));
367    sheet.setColumnWidth(17, columnPixels(20));
368    sheet.setColumnWidth(18, columnPixels(20));
369    sheet.setColumnWidth(34, columnPixels(100));
370
371    int i = titles.length - 1;
372    for (StructureDefinitionMappingComponent map : def.getMapping()) {
373      i++;
374      sheet.setColumnWidth(i, columnPixels(50));
375      sheet.autoSizeColumn(i);
376//      sheet.setColumnHidden(i,  true);
377    }    
378    sheet.createFreezePane(2,1);
379    
380    if (hideMustSupportFalse) {
381    SheetConditionalFormatting sheetCF = sheet.getSheetConditionalFormatting();
382    String address = "A2:AI" + Math.max(Integer.valueOf(sheet.getLastRowNum()), 2);
383    CellRangeAddress[] regions = {
384        CellRangeAddress.valueOf(address)
385    };
386
387    ConditionalFormattingRule rule1 = sheetCF.createConditionalFormattingRule("$G2<>\"Y\"");
388    PatternFormatting fill1 = rule1.createPatternFormatting();
389    fill1.setFillBackgroundColor(IndexedColors.GREY_25_PERCENT.index);
390    fill1.setFillPattern(PatternFormatting.SOLID_FOREGROUND);
391
392    ConditionalFormattingRule rule2 = sheetCF.createConditionalFormattingRule("$Q2<>\"\"");
393    FontFormatting font = rule2.createFontFormatting();
394    font.setFontColorIndex(IndexedColors.GREY_25_PERCENT.index);
395    font.setFontStyle(true, false);
396
397    sheetCF.addConditionalFormatting(regions, rule1, rule2);
398
399    sheet.setAutoFilter(new CellRangeAddress(0,sheet.getLastRowNum(), 0, titles.length+def.getMapping().size() - 1));
400    
401    XSSFSheet xSheet = (XSSFSheet)sheet;
402
403    CTAutoFilter sheetFilter = xSheet.getCTWorksheet().getAutoFilter();
404    CTFilterColumn filterColumn1 = sheetFilter.addNewFilterColumn();
405    filterColumn1.setColId(6);
406    CTCustomFilters filters = filterColumn1.addNewCustomFilters();
407    CTCustomFilter filter1 = filters.addNewCustomFilter();
408    filter1.setOperator(STFilterOperator.NOT_EQUAL);
409    filter1.setVal(" ");
410    
411    CTFilterColumn filterColumn2 = sheetFilter.addNewFilterColumn();
412    filterColumn2.setColId(26);
413    CTFilters filters2 = filterColumn2.addNewFilters();
414    filters2.setBlank(true);
415
416    // We have to apply the filter ourselves by hiding the rows: 
417    for (Row row : sheet) {
418      if (row.getRowNum()>0 && (!row.getCell(6).getStringCellValue().equals("Y") || !row.getCell(26).getStringCellValue().isEmpty())) {
419        ((XSSFRow) row).getCTRow().setHidden(true);
420        }
421      }
422    }
423    sheet.setActiveCell(new CellAddress(sheet.getRow(1).getCell(0)));
424
425    wb.write(outStream);
426    
427    flush();
428    close();
429  }
430
431}