
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}