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}