
001package org.hl7.fhir.r5.formats; 002 003import org.hl7.fhir.exceptions.FHIRFormatError; 004import org.hl7.fhir.r5.model.*; 005import org.hl7.fhir.r5.terminologies.CodeSystemUtilities; 006import org.hl7.fhir.r5.utils.UserDataNames; 007import org.hl7.fhir.utilities.CommaSeparatedStringBuilder; 008import org.hl7.fhir.utilities.FileUtilities; 009import org.hl7.fhir.utilities.Utilities; 010import org.hl7.fhir.utilities.ZipGenerator; 011import org.hl7.fhir.utilities.npm.FilesystemPackageCacheManager; 012import org.hl7.fhir.utilities.npm.NpmPackage; 013 014import java.io.*; 015import java.nio.charset.StandardCharsets; 016import java.util.ArrayList; 017import java.util.HashMap; 018import java.util.List; 019import java.util.Map; 020 021public class CsvFormat { 022 023 public static void main(String[] args) throws Exception { 024 } 025 private final JsonParser json = new JsonParser(); 026 private final Map<String, Integer> codingsCache = new HashMap<>(); 027 028 public void compose(File folder, String baseName, CodeSystem cs) throws FHIRFormatError, IOException { 029 ZipGenerator zip = new ZipGenerator(Utilities.path(folder, baseName + ".zip")); 030 031 // -1-metadata 032 CSVWriter metadata = new CSVWriter(false); 033 metadata.headings("Field", "Value"); 034 metadata.line("url", cs.getUrl()); 035 metadata.line("version", cs.getVersion()); 036 metadata.line("language", cs.getLanguage()); 037 metadata.line("name", cs.getName()); 038 metadata.line("title", cs.getTitle()); 039 metadata.line("status", cs.getStatus().toCode()); 040 metadata.line("experimental", cs.getExperimentalElement().primitiveValue()); 041 metadata.line("date", cs.getDateElement().primitiveValue()); 042 metadata.line("caseSensitive", cs.getCaseSensitiveElement().primitiveValue()); 043 metadata.line("valueSet", cs.getValueSet()); 044 metadata.line("hierarchyMeaning", cs.getHierarchyMeaningElement().primitiveValue()); 045 metadata.line("compositional", cs.getCompositionalElement().primitiveValue()); 046 metadata.line("versionNeeded", cs.getVersionNeededElement().primitiveValue()); 047 metadata.line("content", cs.getContentElement().primitiveValue()); 048 metadata.line("supplements", cs.getSupplements()); 049 metadata.line("count", ""+cs.getCount()); 050 CodeSystem cst = cs.copyHeader(); 051 cst.setText(null); 052 String json = new JsonParser().setOutputStyle(IParser.OutputStyle.NORMAL).composeString(cst); 053 metadata.line("json", json); 054 metadata.close(); 055 zip.addBytes("1-metadata.csv", metadata.close(), false); 056 057 CSVWriter codings = new CSVWriter(true); 058 codings.headings("System", "Version", "Code", "Display"); 059 060 CSVWriter extensions = new CSVWriter(true); 061 extensions.headings("Table", "TableKey", "TableColumn", "URL", "Type", "Value"); 062 063 CSVWriter filters = new CSVWriter(true); 064 filters.headings("Code", "Description", "Value", "Operators"); 065 for (CodeSystem.CodeSystemFilterComponent filter : cs.getFilter()) { 066 CommaSeparatedStringBuilder b = new CommaSeparatedStringBuilder("|"); 067 for (Enumeration<Enumerations.FilterOperator> op : filter.getOperator()) { 068 b.append(op.primitiveValue()); 069 } 070 filters.line(filter.getCode(), filter.getDescription(), filter.getValue(), b.toString()); 071 encodeExtensions(extensions, filter, "filter", filters.lineCount, null); 072 encodeExtensions(extensions, filter.getCodeElement(), "filter", filters.lineCount, "code"); 073 encodeExtensions(extensions, filter.getDescriptionElement(), "filter", filters.lineCount, "description"); 074 encodeExtensions(extensions, filter.getValueElement(), "filters", filters.lineCount, "Value"); 075 } 076 zip.addBytes("3-filters.csv", filters.close(), false); 077 filters.close(); 078 079 CSVWriter properties = new CSVWriter(true); 080 properties.headings("Code", "Uri", "Description", "Type", "Column"); 081 for (CodeSystem.PropertyComponent prop : cs.getProperty()) { 082 int count = surveyCodeSystemProperties(cs.getConcept(), prop.getCode()); 083 if (count == 1) { 084 properties.line(prop.getCode(), prop.getUri(), prop.getDescription(), prop.getTypeElement().primitiveValue(), "P-"+prop.getCode()); 085 prop.setUserData(UserDataNames.db_column, "P-"+prop.getCode()); 086 } else { 087 properties.line(prop.getCode(), prop.getUri(), prop.getDescription(), prop.getTypeElement().primitiveValue()); 088 } 089 encodeExtensions(extensions, prop, "properties", properties.lineCount, null); 090 encodeExtensions(extensions, prop.getCodeElement(), "properties", properties.lineCount, "Code"); 091 encodeExtensions(extensions, prop.getUriElement(), "properties", properties.lineCount, "Uri"); 092 encodeExtensions(extensions, prop.getDescriptionElement(), "properties", properties.lineCount, "Description"); 093 encodeExtensions(extensions, prop.getTypeElement(), "properties", properties.lineCount, "Type"); 094 } 095 zip.addBytes("4-properties.csv", properties.close(), false); 096 097 List<String> headers = new ArrayList<>(); 098 headers.add("Code"); 099 headers.add("Parent"); 100 headers.add("Display"); 101 headers.add("Definition"); 102 for (CodeSystem.PropertyComponent prop : cs.getProperty()) { 103 String col = prop.getUserString(UserDataNames.db_column); 104 if (col != null) { 105 headers.add(col); 106 } 107 } 108 109 CSVWriter concepts = new CSVWriter(true); 110 concepts.headings(headers.toArray(new String[headers.size()])); 111 112 properties = new CSVWriter(true); 113 properties.headings("Code", "Value", "ValueCoding"); 114 115 CSVWriter designations = new CSVWriter(true); 116 designations.headings("Language", "Use", "AdditionalUse", "Value"); 117 118 for (CodeSystem.ConceptDefinitionComponent concept : cs.getConcept()) { 119 composeConcept(cs, concept, null, concepts, properties, designations, codings, extensions); 120 } 121 122 zip.addBytes("2-codings.csv", codings.close(), false); 123 zip.addBytes("5-concepts.csv", concepts.close(), false); 124 zip.addBytes("6-concept-properties.csv", properties.close(), false); 125 zip.addBytes("7-designations.csv", designations.close(), false); 126 zip.addBytes("8-extensions.csv", extensions.close(), false); 127 zip.close(); 128 } 129 130 private void encodeExtensions(CSVWriter extensions, Element element, String table, int tableKey, String tableColumn) throws IOException { 131 if (element instanceof BackboneElement) { 132 if (((BackboneElement) element).hasModifierExtension()) { 133 throw new FHIRFormatError("Modifier Extensions are not supported in the CSV format"); 134 } 135 } 136 if (element instanceof BackboneType) { 137 if (((BackboneType) element).hasModifierExtension()) { 138 throw new FHIRFormatError("Modifier Extensions are not supported in the CSV format"); 139 } 140 } 141 for (Extension ext : element.getExtension()) { 142 if (ext.getValue() == null) { 143 int line = extensions.line(table, ""+tableKey, tableColumn, ext.getUrl(), ext.getValue().fhirType(), null); 144 encodeExtensions(extensions, ext, "extensions", line, null); 145 } else if (ext.getValue() instanceof PrimitiveType<?>) { 146 extensions.line(table, ""+tableKey, tableColumn, ext.getUrl(), ext.getValue().fhirType(), ext.getValue().primitiveValue()); 147 } else { 148 extensions.line(table, ""+tableKey, tableColumn, ext.getUrl(), ext.getValue().fhirType(), json.composeString(ext.getValue(), ext.getValue().fhirType())); 149 } 150 } 151 } 152 153 private void composeConcept(CodeSystem cs, CodeSystem.ConceptDefinitionComponent concept, CodeSystem.ConceptDefinitionComponent parent, 154 CSVWriter concepts, CSVWriter properties, CSVWriter designations, CSVWriter codings, CSVWriter extensions) throws IOException { 155 int ckey = concepts.cells(concept.getCode(), parent == null ? null : ""+parent.getUserInt(UserDataNames.db_key), concept.getDisplay(), concept.getDefinition()); 156 concept.setUserData(UserDataNames.db_key, ckey); 157 encodeExtensions(extensions, concept, "concepts", ckey, null); 158 encodeExtensions(extensions, concept.getCodeElement(), "concepts", ckey, "Code"); 159 encodeExtensions(extensions, concept.getDisplayElement(), "concepts", ckey, "Display"); 160 encodeExtensions(extensions, concept.getDefinitionElement(), "concepts", ckey, "Definition"); 161 162 for (CodeSystem.ConceptDefinitionDesignationComponent d : concept.getDesignation()) { 163 designations.line(d.getLanguage(), ""+coding(d.getUse(), codings, extensions), null, d.getValue()); 164 encodeExtensions(extensions, d, "designations", ckey, null); 165 encodeExtensions(extensions, d.getLanguageElement(), "designations", ckey, "Language"); 166 encodeExtensions(extensions, d.getValueElement(), "designations", ckey, "Value"); 167 } 168 169 List<CodeSystem.ConceptPropertyComponent> handled = new ArrayList<>(); 170 for (CodeSystem.PropertyComponent prop : cs.getProperty()) { 171 String col = prop.getUserString(UserDataNames.db_column); 172 if (col != null) { 173 CodeSystem.ConceptPropertyComponent pv = CodeSystemUtilities.getProperty(concept, prop.getCode()); 174 if (pv != null) { 175 handled.add(pv); 176 concepts.cell(pv.getValue().primitiveValue()); 177 } else { 178 concepts.cell(""); 179 } 180 } 181 } 182 concepts.closeLine(); 183 184 for (CodeSystem.ConceptPropertyComponent pv : concept.getProperty()) { 185 if (!handled.contains(pv)) { 186 if (pv.getValue() instanceof Coding) { 187 properties.line(pv.getCode(), null, ""+coding((Coding) pv.getValue(), codings, extensions)); 188 } else { 189 properties.line(pv.getCode(), pv.getValue().primitiveValue(), null); 190 encodeExtensions(extensions, pv.getValue(), "concept-properties", properties.lineCount, "value"); 191 } 192 encodeExtensions(extensions, pv, "concept-properties", properties.lineCount, null); 193 } 194 } 195 196 for (CodeSystem.ConceptDefinitionComponent c : concept.getConcept()) { 197 composeConcept(cs, c, concept, concepts, properties, designations, codings, extensions); 198 } 199 } 200 201 private int coding(Coding c, CSVWriter codings, CSVWriter extensions) throws IOException { 202 String key = json.composeString(c, "Coding"); 203 if (codingsCache.containsKey(key)) { 204 return codingsCache.get(key); 205 } 206 codings.line(c.getSystem(), c.getVersion(), c.getCode(), c.getDisplay()); 207 encodeExtensions(extensions, c, "Codings", codings.lineCount, null); 208 encodeExtensions(extensions, c.getSystemElement(), "Codings", codings.lineCount, "System"); 209 encodeExtensions(extensions, c.getVersionElement(), "Codings", codings.lineCount, "Version"); 210 encodeExtensions(extensions, c.getCodeElement(), "Codings", codings.lineCount, "Code"); 211 encodeExtensions(extensions, c.getDisplayElement(), "Codings", codings.lineCount, "Display"); 212 codingsCache.put(key, codings.lineCount); 213 return codings.lineCount; 214 // extensions 215 } 216 217 private int surveyCodeSystemProperties(List<CodeSystem.ConceptDefinitionComponent> concept, String code) { 218 int count = 0; 219 for (CodeSystem.ConceptDefinitionComponent ct : concept) { 220 for (CodeSystem.ConceptPropertyComponent p : ct.getProperty()) { 221 int c = 0; 222 if (p.getValue() != null && p.getCode().equals(code)) { 223 if (p.getValue() instanceof Coding || p.getValue().hasExtension()) { 224 return -1; 225 } else { 226 c++; 227 } 228 } 229 count = Integer.max(count, c); 230 } 231 count = Integer.max(count, surveyCodeSystemProperties(ct.getConcept(), code)); 232 } 233 return count; 234 } 235 236 private class CSVWriter { 237 private int lineCount = 0; 238 private boolean autoKey; 239 private Writer writer; 240 private int colCount = 0; 241 private ByteArrayOutputStream stream; 242 243 public CSVWriter(boolean autoKey) { 244 this.autoKey = autoKey; 245 this.stream = new ByteArrayOutputStream(); 246 this.writer = new OutputStreamWriter(stream, StandardCharsets.UTF_8); 247 } 248 249 /** 250 * Escapes a cell value according to CSV rules: 251 * - Enclose in quotes if contains comma, quote, newline, or carriage return 252 * - Double any quotes within the value 253 */ 254 private String escapeCell(String cell) { 255 if (cell == null) { 256 return ""; 257 } 258 // Check if escaping is needed 259 if (cell.contains(",") || cell.contains("\"") || 260 cell.contains("\n") || cell.contains("\r")) { 261 // Escape double quotes by doubling them 262 cell = cell.replace("\r", "\\r"); 263 cell = cell.replace("\n", "\\n"); 264 String escaped = cell.replace("\"", "\"\""); 265 return "\"" + escaped + "\""; 266 } 267 return cell; 268 } 269 270 public void headings(String... cells) throws IOException { 271 if (autoKey) { 272 writer.write("Key"); 273 colCount++; 274 } 275 writeCells(cells); 276 closeLine(); 277 } 278 279 public int line(String... cells) throws IOException { 280 lineCount++; // Increment when line starts being written 281 if (autoKey) { 282 writer.write(String.valueOf(lineCount)); 283 colCount++; 284 } 285 writeCells(cells); 286 closeLine(); 287 return lineCount; 288 } 289 290 public int cells(String... cells) throws IOException { 291 lineCount++; // Increment when line starts being written 292 if (autoKey) { 293 writer.write(String.valueOf(lineCount)); 294 colCount++; 295 } 296 writeCells(cells); 297 return lineCount; 298 } 299 300 public int writeCells(String... cells) throws IOException { 301 for (int i = 0; i < cells.length; i++) { 302 if (colCount > 0) { 303 writer.write(","); 304 } 305 if (!Utilities.noString(cells[i])) { 306 writer.write(escapeCell(cells[i])); 307 } 308 colCount++; 309 } 310 return lineCount; 311 } 312 313 public byte[] close() throws IOException { 314 if (writer != null) { 315 writer.close(); 316 } 317 return stream.toByteArray(); 318 } 319 320 public void cell(String s) throws IOException { 321 writeCells(s); 322 } 323 324 public void closeLine() throws IOException { 325 writer.write("\r\n"); 326 writer.flush(); 327 colCount = 0; 328 } 329 } 330 331}