All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.imsweb.layout.naaccrxml.NaaccrXmlLayout Maven / Gradle / Ivy

Go to download

Framework that allows defining file formats (layouts) and use them to read and write data files.

There is a newer version: 6.0
Show newest version
/*
 * Copyright (C) 2017 Information Management Services, Inc.
 */
package com.imsweb.layout.naaccrxml;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Reader;
import java.io.StringWriter;
import java.io.Writer;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.zip.ZipInputStream;

import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;

import com.imsweb.layout.Layout;
import com.imsweb.layout.LayoutInfo;
import com.imsweb.layout.LayoutInfoDiscoveryOptions;
import com.imsweb.layout.LayoutUtils;
import com.imsweb.naaccrxml.NaaccrFormat;
import com.imsweb.naaccrxml.NaaccrIOException;
import com.imsweb.naaccrxml.NaaccrOptions;
import com.imsweb.naaccrxml.NaaccrXmlDictionaryUtils;
import com.imsweb.naaccrxml.NaaccrXmlUtils;
import com.imsweb.naaccrxml.PatientXmlReader;
import com.imsweb.naaccrxml.PatientXmlWriter;
import com.imsweb.naaccrxml.entity.NaaccrData;
import com.imsweb.naaccrxml.entity.Patient;
import com.imsweb.naaccrxml.entity.dictionary.NaaccrDictionary;
import com.imsweb.naaccrxml.entity.dictionary.NaaccrDictionaryItem;

import static com.imsweb.naaccrxml.NaaccrXmlUtils.NAACCR_XML_ROOT_ATT_BASE_DICT;
import static com.imsweb.naaccrxml.NaaccrXmlUtils.NAACCR_XML_ROOT_ATT_REC_TYPE;
import static com.imsweb.naaccrxml.NaaccrXmlUtils.NAACCR_XML_ROOT_ATT_USER_DICT;
import static java.nio.charset.StandardCharsets.UTF_8;

/**
 * This class contains the logic related to all NAACCR XML layouts
 */
public class NaaccrXmlLayout implements Layout {

    // CSS styles for the fields HTML documentation
    private static final StringBuilder _CSS_STYLE = new StringBuilder();

    static {
        // style for the summary tables
        _CSS_STYLE.append(".naaccr-summary-table { width: 100%; }\n");
        _CSS_STYLE.append(".naaccr-summary-header { text-align: center; padding: 2px; background-color: #E0E0E0; }\n");
        _CSS_STYLE.append(".naaccr-summary-cell { vertical-align:top; padding: 2px; }\n");
        _CSS_STYLE.append(".naaccr-summary-centered { text-align: center; }\n");
        _CSS_STYLE.append(".naaccr-borders { border: 1px solid gray; border-collapse: collapse; }\n");
        _CSS_STYLE.append(".naaccr-underline { text-decoration:underline; }\n");

        // style for everything else
        _CSS_STYLE.append("#readerWrapper { margin-top: 20px;}\n");
        _CSS_STYLE.append("#pnlSearch { width: 100%;}\n");
        _CSS_STYLE.append("#lblSearch { margin-right: 20px;}\n");
        _CSS_STYLE.append(".wrapper { margin-bottom: 50px; width: 775px;}\n");
        _CSS_STYLE.append(
                ".col { border-bottom: 1px solid black; border-left: 1px solid black; border-top: 1px solid black; float: left; height: 1100px; margin-bottom: 50px; padding: 0 5px; width: 180px;}\n");
        _CSS_STYLE.append(
                ".colnoborder { border-bottom: 1px solid black; border-top: 1px solid black; float: left; height: 1100px; margin-bottom: 50px; padding: 0 5px; width: 180px;}\n");
        _CSS_STYLE.append(".tableColHead td:first-child { text-align: left;}\n");
        _CSS_STYLE.append(".tableColBody { text-align: center; vertical-align: top;}\n");
        _CSS_STYLE.append(".tableColTitle { background-color: #165185; color: #FABA44;}\n");
        _CSS_STYLE.append(".tableColDataStripe td:first-child { border-left: 0 none;}\n");
        _CSS_STYLE.append(".tableColDataStripe table tr td { padding: 0; text-align: center;}\n");
        _CSS_STYLE.append(".tableColData td:first-child { border-left: 0 none;}\n");
        _CSS_STYLE.append(".tableColData table tr td { padding: 0; text-align: center;}\n");
        _CSS_STYLE.append(".tableColData td.HighlightToolTip,\n");
        _CSS_STYLE.append(".tableColDataStripe td.HighlightToolTip { border-bottom: 1px dashed black; background-color: #a7d9f2;}\n");
        _CSS_STYLE.append(".tableAppendixBody { border-left: 0 none; display: table-row; padding-bottom: 1px; text-align: left;}\n");
        _CSS_STYLE.append(".code-nbr { padding-right: 20px; vertical-align: top;}\n");
        _CSS_STYLE.append("#stateList { padding-bottom: 30px; text-align: center; width: 720px;}\n");
        _CSS_STYLE.append("#chapter { width: 950px;}\n");
        _CSS_STYLE.append("#tiptip_holder { display: none; left: 0; position: absolute; top: 0; z-index: 99999;}\n");
        _CSS_STYLE.append("#tiptip_holder.tip_top { padding-bottom: 5px;}\n");
        _CSS_STYLE.append("#tiptip_holder.tip_bottom { padding-top: 5px;}\n");
        _CSS_STYLE.append("#tiptip_holder.tip_right { padding-left: 5px;}\n");
        _CSS_STYLE.append("#tiptip_holder.tip_left { padding-right: 5px;}\n");
        _CSS_STYLE.append("#tblSearch { margin-top: 20px;}\n");
        _CSS_STYLE.append(".dataDictionaryHeader { padding-right: 10px; text-align: right;}\n");
        _CSS_STYLE.append("#menuWrapper { height: 200px; margin-top: 20px; width: 950px;}\n");
        _CSS_STYLE.append(".chapterColumn { border-right: 2px solid white; float: left;}\n");
        _CSS_STYLE.append("a .chapter { color: #FFFFFF; padding: 0; text-decoration: none;}\n");
        _CSS_STYLE.append(".chapter:hover { background: none repeat scroll 0 0 #FF9900; color: #FFFFFF; text-decoration: none;}\n");
        _CSS_STYLE.append("#tiptip_arrow, #tiptip_arrow_inner { border-color: transparent; border-style: solid; border-width: 6px; height: 0; position: absolute; width: 0;}\n");
        _CSS_STYLE.append("#tiptip_holder.tip_top #tiptip_arrow { border-top-color: #FFFFCE;}\n");
        _CSS_STYLE.append("#tiptip_holder.tip_bottom #tiptip_arrow { border-bottom-color: #FFFFCE;}\n");
        _CSS_STYLE.append("#tiptip_holder.tip_right #tiptip_arrow { border-right-color: #FFFFCE;}\n");
        _CSS_STYLE.append("#tiptip_holder.tip_left #tiptip_arrow { border-left-color: #FFFFCE;}\n");
        _CSS_STYLE.append("#tiptip_holder.tip_top #tiptip_arrow_inner { border-top-color: #FFFFCE; margin-left: -6px; margin-top: -7px;}\n");
        _CSS_STYLE.append("#tiptip_holder.tip_bottom #tiptip_arrow_inner { border-bottom-color: #FFFFCE; margin-left: -6px; margin-top: -5px;}\n");
        _CSS_STYLE.append("#tiptip_holder.tip_right #tiptip_arrow_inner { border-right-color: #FFFFCE; margin-left: -5px; margin-top: -6px;}\n");
        _CSS_STYLE.append("#tiptip_holder.tip_left #tiptip_arrow_inner { border-left-color: #FFFFCE; margin-left: -7px; margin-top: -6px;}\n");
        _CSS_STYLE.append(".GeoCtrAlpha { float: left; width: 300px;}\n");
        _CSS_STYLE.append(".threeColSubTitle { float: left; font-style: italic; margin-bottom: 12px; width: 300px;}\n");
        _CSS_STYLE.append(".colRight { border-bottom: 1px solid black; border-left: 1px solid black; float: right; height: 1150px; padding-left: 20px; width: 454px;}\n");
        _CSS_STYLE.append(".colLeft { border-bottom: 1px solid black; float: left; height: 1150px; padding-left: 20px; width: 455px;}\n");
        _CSS_STYLE.append(".GeoCtr { float: left; width: 460px;}\n");
        _CSS_STYLE.append(".threeCol { border-bottom: 1px solid black; float: left; height: 1135px; padding-left: 10px; width: 305px;}\n");
        _CSS_STYLE.append(".threeColTitle { float: left; font-weight: bold; width: 300px;}\n");
        _CSS_STYLE.append(".CountryContinentName { float: left; font-weight: bold; margin-bottom: 15px; margin-top: 15px; width: 400px;}\n");
        _CSS_STYLE.append(
                ".threeColMid { -moz-border-bottom-colors: none; -moz-border-image: none; -moz-border-left-colors: none; -moz-border-right-colors: none; -moz-border-top-colors: none; border-color: -moz-use-text-color black black; border-style: none solid solid; border-width: medium 1px 1px; float: left; height: 1135px; padding-left: 10px; width: 305px;}\n");
        _CSS_STYLE.append("#tiptip_content { background-color: #FFFFCE; padding: 4px 8px 5px;}\n");
        _CSS_STYLE.append("#tiptip_holder.tip_bottom #tiptip_arrow_inner { border-bottom-color: #FFFFCE;}\n");
        _CSS_STYLE.append("#tiptip_holder.tip_top #tiptip_arrow_inner { border-top-color: rgba(20, 20, 20, 0.92);}\n");
        _CSS_STYLE.append("h1 { margin: 1em 0;}\n");
        _CSS_STYLE.append("h2, h3 { margin: 0 0 1.33em 0;}\n");
        _CSS_STYLE.append("h4 { margin: 0;}\n");
        _CSS_STYLE.append("ul, ol { margin: 0 0 1.33em 35px;}\n");
        _CSS_STYLE.append("ul li, ol li { margin-bottom: 1.33em;}\n");
        _CSS_STYLE.append("table.padded td, table.padded th { padding: 5px;}\n");
        _CSS_STYLE.append("ul.nobullets { list-style: none;}\n");
        _CSS_STYLE.append("ul.notspaced li, ol.notspaced li { margin-bottom: 0;}\n");
        _CSS_STYLE.append(".c8cell1 { width: 70px;}\n");
        _CSS_STYLE.append("#pnlSearch { width: 100%;}\n");
        _CSS_STYLE.append("#lblSearch { margin-right: 20px;}\n");
        _CSS_STYLE.append(".chap10-head-table { margin-top: 15px;}\n");
        _CSS_STYLE.append(".chap10-para-head { font-weight: bold; padding-top: 10px;}\n");
        _CSS_STYLE.append(".chap10-para { padding-bottom: 10px;}\n");
        _CSS_STYLE.append(".chap10-para ul li { margin-bottom: 0;}\n");
    }

    // layout ID
    protected String _layoutId;

    // layout Name
    protected String _layoutName;

    // layout Description
    protected String _layoutDesc;

    // layout version (for this layout type, it's the NAACCR version)
    protected String _naaccrVersion;

    // record type supported by this layout (A, M, C or I)
    private String _recordType;

    // the base dictionary for this layout
    private NaaccrDictionary _baseDictionary;

    // the user-defined dictionaries for this layout
    private List _userDictionaries;

    // the fields for this layout
    private final List _allFields = new ArrayList<>();

    // fields cache for quick access by name (which is NAACCR ID for this layout)
    private final Map _fieldsCachedByName = new HashMap<>();

    // fields cache for quick access by NAACCR number
    private final Map _fieldsCachedByNaaccrNumber = new HashMap<>();

    /**
     * Default constructor.
     */
    public NaaccrXmlLayout() {
    }

    /**
     * Constructor.
     * @param naaccrVersion String of three digit NAACCR version
     * @param recordType String - allowed values are "A", "M", "C", and "I"
     * @param layoutId String used as layout ID - must be unique from those registered in Layout Factory
     * @param layoutName String used as layout name
     * @param dictionaries List (of type NaaccrDictionary) of custom user dictionaries - used for making custom layouts
     * @param loadFields determines whether to load all the fields from all dictionaries into the allFields variable
     */
    @SuppressWarnings("DataFlowIssue")
    public NaaccrXmlLayout(String naaccrVersion, String recordType, String layoutId, String layoutName, String description, List dictionaries, boolean loadFields) {
        _naaccrVersion = naaccrVersion;
        _layoutName = layoutName;
        _layoutId = layoutId;
        _recordType = recordType;
        _baseDictionary = NaaccrXmlDictionaryUtils.getBaseDictionaryByVersion(naaccrVersion);
        _userDictionaries = dictionaries;
        if (_userDictionaries == null || _userDictionaries.isEmpty()) {
            NaaccrDictionary defaultUserDictionary = NaaccrXmlDictionaryUtils.getDefaultUserDictionaryByVersion(naaccrVersion);
            if (defaultUserDictionary != null)
                _userDictionaries = Collections.singletonList(defaultUserDictionary);
            else
                _userDictionaries = Collections.emptyList();
        }
        _layoutDesc = StringUtils.stripToNull(description);

        // only load dictionaries/fields if specified, otherwise avoid expensive operations
        if (loadFields) {

            Map shortLabels = new HashMap<>();
            Map sections = new HashMap<>();
            try (BufferedReader in = new BufferedReader(new InputStreamReader(Thread.currentThread().getContextClassLoader().getResourceAsStream("layout/fixed/naaccr/items-extra-info.csv"), UTF_8))) {
                in.lines().forEach(line -> {
                    String[] parts = StringUtils.split(line, ',');
                    if (parts.length == 3) {
                        shortLabels.put(parts[0], parts[1]);
                        sections.put(parts[0], parts[2]);
                    }
                });
            }
            catch (IOException e) {
                throw new IllegalStateException(e);
            }

            // get all item definitions, create fields and add to layout's field list based on record type
            for (NaaccrDictionaryItem item : NaaccrXmlDictionaryUtils.mergeDictionaries(_baseDictionary, _userDictionaries.toArray(new NaaccrDictionary[0])).getItems()) {
                if (item.getRecordTypes() == null || item.getRecordTypes().isEmpty() || item.getRecordTypes().contains(_recordType)) {
                    NaaccrXmlField field = new NaaccrXmlField(item);
                    field.setShortLabel(shortLabels.getOrDefault(item.getNaaccrId(), "?"));
                    field.setSection(sections.getOrDefault(item.getNaaccrId(), "?"));

                    _allFields.add(field);
                    _fieldsCachedByNaaccrNumber.put(field.getNaaccrItemNum(), field);
                    _fieldsCachedByName.put(field.getNaaccrId(), field);

                    //If the field is a date, add child fields for the year, month and day parts
                    if (NaaccrXmlDictionaryUtils.NAACCR_DATA_TYPE_DATE.equals(item.getDataType())) {
                        String shortLbl = field.getShortLabel();
                        if (shortLbl.endsWith(" Dt"))
                            shortLbl = shortLbl.substring(0, shortLbl.length() - 3);

                        NaaccrDictionaryItem yearItem = new NaaccrDictionaryItem();
                        yearItem.setNaaccrId(item.getNaaccrId() + "Year");
                        yearItem.setNaaccrName(field.getNaaccrName() + " (Year)");
                        yearItem.setParentXmlElement(field.getParentXmlElement());
                        yearItem.setLength(4);
                        yearItem.setDataType(NaaccrXmlDictionaryUtils.NAACCR_DATA_TYPE_DIGITS);
                        NaaccrXmlField yearFld = new NaaccrXmlField(yearItem);
                        yearFld.setShortLabel(shortLbl + " Yr");
                        _fieldsCachedByName.put(yearFld.getNaaccrId(), yearFld);

                        NaaccrDictionaryItem monthItem = new NaaccrDictionaryItem();
                        monthItem.setNaaccrId(item.getNaaccrId() + "Month");
                        monthItem.setNaaccrName(field.getNaaccrName() + " (Month)");
                        monthItem.setParentXmlElement(field.getParentXmlElement());
                        monthItem.setLength(2);
                        monthItem.setDataType(NaaccrXmlDictionaryUtils.NAACCR_DATA_TYPE_DIGITS);
                        NaaccrXmlField monthFld = new NaaccrXmlField(monthItem);
                        monthFld.setShortLabel(shortLbl + " Mth");
                        _fieldsCachedByName.put(monthFld.getNaaccrId(), monthFld);

                        NaaccrDictionaryItem dayItem = new NaaccrDictionaryItem();
                        dayItem.setNaaccrId(item.getNaaccrId() + "Day");
                        dayItem.setNaaccrName(field.getNaaccrName() + " (Day)");
                        dayItem.setParentXmlElement(field.getParentXmlElement());
                        dayItem.setLength(2);
                        dayItem.setDataType(NaaccrXmlDictionaryUtils.NAACCR_DATA_TYPE_DIGITS);
                        NaaccrXmlField dayFld = new NaaccrXmlField(dayItem);
                        dayFld.setShortLabel(shortLbl + " Day");
                        _fieldsCachedByName.put(dayFld.getNaaccrId(), dayFld);

                        field.setSubFields(Arrays.asList(yearFld, monthFld, dayFld));
                    }
                }
            }
        }
        verify();
    }

    /**
     * Verify the internal fields, throws a runtime exception if something is wrong.
     */
    public void verify() {

        // ID is required
        if (_layoutId == null || _layoutId.isEmpty())
            throw new IllegalStateException("Layout ID is required");

        // name is required
        if (_layoutName == null || _layoutName.isEmpty())
            throw new IllegalStateException("Layout name is required");

        // Record type is required and must be one of the following: A, M, C, I
        if (_recordType == null || _recordType.isEmpty())
            throw new IllegalStateException("Record type is required");
        else if (!_recordType.equals("A") && !_recordType.equals("M") && !_recordType.equals("C") && !_recordType.equals("I")) {
            throw new IllegalStateException("Record type not recognized: " + _recordType);
        }

        // NAACCR Version is required and must be a version that is currently supported
        if (_naaccrVersion == null || !NaaccrFormat.isVersionSupported(_naaccrVersion))
            throw new IllegalStateException("Unsupported NAACCR version: " + _naaccrVersion);

        // base dictionary is required
        if (_baseDictionary == null)
            throw new IllegalStateException("Base Dictionary is required");

        // validate the user dictionaries
        for (NaaccrDictionary userDictionary : _userDictionaries) {
            List errors = NaaccrXmlDictionaryUtils.validateUserDictionary(userDictionary);
            if (!errors.isEmpty())
                throw new IllegalStateException("Error found on user dictionary - " + errors.get(0));
        }

        // if fields/dictionaries were supposed to be loaded, check validity of fields and dictionaries. Otherwise, this is the end of validation.
        if (!_allFields.isEmpty()) {

            // validate the NaaccrXmlFields
            Set names = new HashSet<>();
            Set naaccrItemNums = new HashSet<>();
            for (NaaccrXmlField field : _allFields) {
                if (field.getName() == null)
                    throw new IllegalStateException("Field name (NAACCR XML ID) is required");
                if (names.contains(field.getName()))
                    throw new IllegalStateException("Field name (NAACCR XML ID) must be unique, found duplicate name for '" + field.getName() + "'");
                names.add(field.getName());
                if (field.getItem() == null)
                    throw new IllegalStateException("Field item definition is required, missing for field " + field.getName());
                if (field.getNaaccrItemNum() != null) {
                    if (naaccrItemNums.contains(field.getNaaccrItemNum().toString()))
                        throw new IllegalStateException("Field NAACCR number must be unique, found duplicate number for '" + field.getNaaccrItemNum() + "'");
                    naaccrItemNums.add(field.getNaaccrItemNum().toString());
                }
                if (field.getLength() == null)
                    throw new IllegalStateException("Field length is required, missing for field " + field.getName());
                if (field.getParentXmlElement() == null)
                    throw new IllegalStateException("Field parent XML element is required, missing for field " + field.getName());
            }
        }
    }

    @Override
    public String getLayoutId() {
        return _layoutId;
    }

    public void setLayoutId(String layoutId) {
        _layoutId = layoutId;
    }

    @Override
    public String getLayoutName() {
        return _layoutName;
    }

    public void setLayoutName(String layoutName) {
        _layoutName = layoutName;
    }

    @Override
    public String getLayoutVersion() {
        return _naaccrVersion;
    }

    public void setLayoutVersion(String version) {
        _naaccrVersion = version;
    }

    @Override
    public String getLayoutDescription() {
        return _layoutDesc;
    }

    public void setLayoutDescription(String description) {
        _layoutDesc = description;
    }

    public String getNaaccrVersion() {
        return _naaccrVersion;
    }

    public void setNaaccrVersion(String naaccrVersion) {
        _naaccrVersion = naaccrVersion;
    }

    public String getRecordType() {
        return _recordType;
    }

    public void setRecordType(String recordType) {
        _recordType = recordType;
    }

    @Override
    public List getAllFields() {
        return _allFields;
    }

    @Override
    public NaaccrXmlField getFieldByName(String name) {
        return _fieldsCachedByName.get(name);
    }

    @Override
    public NaaccrXmlField getFieldByNaaccrItemNumber(Integer num) {
        return _fieldsCachedByNaaccrNumber.get(num);
    }

    @Override
    public String getFieldDocByName(String name) {
        return getFieldDocByNameOrNumber(name, null, null, null);
    }

    public String getFieldDocByName(String name, File archivedDocFile) {
        return getFieldDocByNameOrNumber(name, null, archivedDocFile, null);
    }

    public String getFieldDocByName(String name, ZipInputStream archivedDocStream) {
        return getFieldDocByNameOrNumber(name, null, null, archivedDocStream);
    }

    @Override
    public String getFieldDocByNaaccrItemNumber(Integer num) {
        return getFieldDocByNameOrNumber(null, num, null, null);
    }

    public String getFieldDocByNaaccrItemNumber(Integer num, File archivedDocFile) {
        return getFieldDocByNameOrNumber(null, num, archivedDocFile, null);
    }

    public String getFieldDocByNaaccrItemNumber(Integer num, ZipInputStream archivedDocStream) {
        return getFieldDocByNameOrNumber(null, num, null, archivedDocStream);
    }

    @SuppressWarnings("java:S1075") // hard-coded path separator
    protected String getFieldDocByNameOrNumber(String name, Integer number, File archivedDocFile, ZipInputStream archivedDocStream) {
        NaaccrXmlField field = name != null ? getFieldByName(name) : getFieldByNaaccrItemNumber(number);

        String filename = null;
        if (field != null)
            filename = field.getName();
        else if (number != null)
            filename = number.toString();

        if (filename == null)
            return null;

        String result = null;

        URL docPath = Thread.currentThread().getContextClassLoader().getResource("layout/fixed/naaccr/doc/" + getDocFolder() + "/" + filename + ".html");
        if (docPath != null) {
            try (Reader reader = new InputStreamReader(docPath.openStream(), StandardCharsets.UTF_8); Writer writer = new StringWriter()) {
                IOUtils.copy(reader, writer);
                result = writer.toString();
            }
            catch (IOException e) {
                /* do nothing, result will be null, as per specs */
            }
        }
        else if (archivedDocFile != null && archivedDocFile.exists())
            result = LayoutUtils.readNaaccrDocumentationFromFile(getDocFolder() + "/" + filename + ".html", archivedDocFile);
        else if (archivedDocStream != null)
            result = LayoutUtils.readNaaccrDocumentationFromZipInputStream(getDocFolder() + "/" + filename + ".html", archivedDocStream);

        return result;
    }

    protected String getDocFolder() {
        // there is always a delay before the documentation is released on the NAACCR website...
        if ("250".equals(_naaccrVersion))
            return "naaccr24";
        return "naaccr" + _naaccrVersion.substring(0, 2);
    }

    @Override
    public String getFieldDocDefaultCssStyle() {
        return _CSS_STYLE.toString();
    }

    public void setFields(Collection fields) {
        _allFields.clear();
        _allFields.addAll(fields);
        verify();
    }

    public NaaccrDictionary getBaseDictionary() {
        return _baseDictionary;
    }

    public void setBaseDictionary(NaaccrDictionary dictionary) {
        _baseDictionary = dictionary;
    }

    public List getUserDictionaries() {
        return _userDictionaries;
    }

    public void setUserDictionaries(List dictionaries) {
        _userDictionaries = dictionaries;
    }

    /**
     * Writes a single patient using a provided writer. Does not open or close the writer.
     * @param writer The PatientXmlWriter used to write Patients to an XML file
     * @param patient The patient that is to be written
     * @throws NaaccrIOException if patients cannot be written
     */
    public void writeNextPatient(PatientXmlWriter writer, Patient patient) throws NaaccrIOException {
        if (patient != null)
            writer.writePatient(patient);
    }

    /**
     * Opens a FileOutputStream to write a list of patients to a file. Passes the stream to the method below.
     * The first step of this process is to write the root data to the file, so this method can only be used to write a complete
     * list of patients (it cannot write a "partial" list that can be added to later).
     * The OutputStream is opened and closed in this method.
     * @param file File that the patients will be written to
     * @param allPatients List of all patients that will be written to the file.
     * @param data The root data, to be written at the top of the file.
     * @throws NaaccrIOException if patients cannot be written
     */
    public void writeAllPatients(File file, List allPatients, NaaccrData data, NaaccrOptions options) throws NaaccrIOException {
        try (FileOutputStream os = new FileOutputStream(file)) {
            writeAllPatients(os, allPatients, data, options);
        }
        catch (IOException e) {
            throw new NaaccrIOException(e.getMessage());
        }
    }

    /**
     * Starts a PatientXmlWriter to write a list of patients to an OutputStream. Passes the writer to the method below.
     * The first step of this process is to write the root data to the stream, so this method can only be used to write a complete
     * list of patients (it cannot write a "partial" list that can be added to later).
     * This method does not open or close the OutputStream.
     * The writer is opened and closed in this method.
     * @param outputStream The OutputStream that the writer will write to
     * @param patients List of Patients to be written
     * @param data Root data needed to create a PatientXmlWriter - is written as part of the writer's construction
     * @throws NaaccrIOException if patients cannot be written
     */
    @SuppressWarnings("java:S2093") // use try-with-resources; this method doesn't close the writer!
    public void writeAllPatients(OutputStream outputStream, List patients, NaaccrData data, NaaccrOptions options) throws NaaccrIOException {
        if (data == null)
            return;

        PatientXmlWriter writer = null;
        try {
            writer = new PatientXmlWriter(new OutputStreamWriter(outputStream, UTF_8), data, options, _userDictionaries);
            writeAllPatients(writer, patients);
        }
        finally {
            if (writer != null)
                writer.closeAndKeepAlive();
        }
    }

    /**
     * Used to write a list of patients using a PatientXmlWriter. Can be multiple times to write to the same file.
     * This method does not open or close the writer.
     * @param writer The PatientXmlWriter used to write Patients to an XML file
     * @param patients List of Patients to be written
     * @throws NaaccrIOException if patients cannot be written
     */
    public void writeAllPatients(PatientXmlWriter writer, List patients) throws NaaccrIOException {
        if (patients != null)
            for (Patient patient : patients)
                writer.writePatient(patient);
    }

    /**
     * Used to read single patients from an XML file using a PatientXmlReader
     * @param reader PatientXmlReader used to read Patients from an XML file.
     * @return returns the next Patient found by the reader
     * @throws NaaccrIOException if patients cannot be read
     */
    public Patient readNextPatient(PatientXmlReader reader) throws NaaccrIOException {
        return reader.readPatient();
    }

    /**
     * Opens a FileInputStream to read all the Patients from an XML file. Passes the stream to the method below.
     * The method opens and closes the InputStream.
     * @param file XML file that Patients will be read from.
     * @param encoding The encoding of the file (null means default OS encoding)
     * @return List of all Patients in the file.
     * @throws NaaccrIOException if patients cannot be read
     */
    public List readAllPatients(File file, String encoding, NaaccrOptions options) throws NaaccrIOException {
        List patients;

        try (FileInputStream is = new FileInputStream(file)) {
            patients = readAllPatients(is, encoding, options);
        }
        catch (IOException e) {
            throw new NaaccrIOException(e.getMessage());
        }

        return patients;
    }

    /**
     * Creates a PatientXmlReader from the InputStream parameter to read all Patients from an XML file. Passes the writer to the method below.
     * This method opens and closes the reader.
     * This method does not open or close the InputStream.
     * @param inputStream the InputStream used to read the patients (required)
     * @param encoding Stream encoding (required)
     * @param options (can be null)
     * @return List of all Patients read from the stream.
     * @throws NaaccrIOException if patients cannot be read
     */
    @SuppressWarnings("java:S2093") // use try-with-resources; this method doesn't close the writer!
    public List readAllPatients(InputStream inputStream, String encoding, NaaccrOptions options) throws NaaccrIOException {
        List patients;

        PatientXmlReader reader = null;
        try {
            reader = new PatientXmlReader(new InputStreamReader(inputStream, encoding), options, _userDictionaries);
            patients = readAllPatients(reader);
        }
        catch (IOException e) {
            throw new NaaccrIOException(e.getMessage());
        }
        finally {
            if (reader != null)
                reader.closeAndKeepAlive();
        }
        return patients;
    }

    /**
     * Reads all Patients found by the reader.
     * This method does not open or close the reader.
     * @param reader PatientXmlReader used to read Patients from an XML file
     * @return List of all the Patients found by the reader.
     * * @throws NaaccrIOException if patients cannot be read
     */
    public List readAllPatients(PatientXmlReader reader) throws NaaccrIOException {
        List allPatients = new ArrayList<>();
        Patient patient;
        while ((patient = reader.readPatient()) != null)
            allPatients.add(patient);

        return allPatients;
    }

    /**
     * Compares the record type and dictionaries to determine if this layout can read the input file
     * @param file data file
     * @param zipEntryName optional zip entry to use if the file is a zip file, not used if the file is not a zip file
     * @param options discovery options
     * @return LayoutInfo containing this layout's ID and name. Null if this layout cannot be used to read this file.
     */
    @Override
    public LayoutInfo buildFileInfo(File file, String zipEntryName, LayoutInfoDiscoveryOptions options) {
        if (file == null)
            return null;

        Map attr;
        try (InputStreamReader is = new InputStreamReader(LayoutUtils.createInputStream(file, zipEntryName), UTF_8)) {
            attr = NaaccrXmlUtils.getAttributesFromXmlReader(is);
        }
        catch (IOException e) {
            return null;
        }

        String recordType = attr.get(NAACCR_XML_ROOT_ATT_REC_TYPE);
        if (recordType == null || recordType.isEmpty() || !_recordType.equals(recordType))
            return null;

        String baseUri = NaaccrXmlDictionaryUtils.createUriFromVersion(_naaccrVersion, true);
        if (!baseUri.equals(attr.get(NAACCR_XML_ROOT_ATT_BASE_DICT)))
            return null;

        LayoutInfo info = new LayoutInfo();
        info.setLayoutId(_layoutId);
        info.setLayoutName(_layoutName);
        info.setAvailableUserDictionaries(_userDictionaries == null ? Collections.emptyList() : _userDictionaries.stream().map(NaaccrDictionary::getDictionaryUri).collect(Collectors.toList()));
        info.setRequestedUserDictionaries(attr.get(NAACCR_XML_ROOT_ATT_USER_DICT) == null ? Collections.emptyList() : Arrays.asList(StringUtils.split(attr.get(NAACCR_XML_ROOT_ATT_USER_DICT), " ")));

        // at this point we know that this layout can be used to read the data file; let's try to get the root data and if anything goes wrong,
        // let's return the info object, without the root data and with the error
        NaaccrOptions opts = NaaccrOptions.getDefault();
        opts.setUseStrictNamespaces(options == null || options.isNaaccrXmlUseStrictNamespaces());
        opts.setIgnoreExtensions(true);
        try (InputStreamReader is = new InputStreamReader(LayoutUtils.createInputStream(file, zipEntryName), UTF_8); PatientXmlReader reader = new PatientXmlReader(is, opts, _userDictionaries)) {
            info.setRootNaaccrXmlData(reader.getRootData());
        }
        catch (IOException e) {
            info.setErrorMessage(e.getMessage());
        }

        return info;
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy