Package org.gedcom4j.writer

Source Code of org.gedcom4j.writer.GedcomWriter

/*
* Copyright (c) 2009-2014 Matthew R. Harrah
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package org.gedcom4j.writer;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.List;

import org.gedcom4j.io.GedcomFileWriter;
import org.gedcom4j.model.*;
import org.gedcom4j.validate.GedcomValidationFinding;
import org.gedcom4j.validate.GedcomValidator;
import org.gedcom4j.validate.Severity;

/**
* <p>
* A class for writing the gedcom structure out as a GEDCOM 5.5 compliant file.
* </p>
* <p>
* General usage is as follows:
* <ul>
* Instantiate a <code>GedcomWriter</code>, passing the {@link Gedcom} to be written as a parameter to the constructor.
* </ul>
* <ul>
* Call one of the variants of the <code>write</code> method to write the data
* </ul>
* <ul>
* Optionally check the contents of the {@link #validationFindings} collection to see if there was anything problematic
* found in your data.
* </ul>
* </p>
*
* <h3>Validation</h3>
* <p>
* By default, this class automatically validates your data prior to writing it by using a
* {@link org.gedcom4j.validate.GedcomValidator}. This is to prevent writing data that does not conform to the spec.
* </p>
*
* <p>
* If validation finds any errors that are of severity ERROR, the writer will throw an Exception (usually
* {@link GedcomWriterVersionDataMismatchException} or {@link GedcomWriterException}). If this occurs, check the
* {@link #validationFindings} collection to determine what the problem was.
* </p>
*
* <p>
* Although validation is automatically performed, autorepair is turned off by default (see
* {@link org.gedcom4j.validate.GedcomValidator#autorepair})...this way your data is not altered. Validation can be
* suppressed if you want by setting {@link #validationSuppressed} to true, but this is not recommended. You can also
* force autorepair on if you want.
* </p>
*
* @author frizbog1
*/
public class GedcomWriter {

    /**
     * The maximum length of lines to be written out before splitting
     */
    private static final int MAX_LINE_LENGTH = 128;

    /**
     * The Gedcom data to write
     */
    private final Gedcom gedcom;

    /**
     * Are we suppressing the call to the validator? Deliberately package-private so unit tests can fiddle with it to
     * make testing easy.
     */
    boolean validationSuppressed = false;

    /**
     * The lines of the GEDCOM transmission, which will be written using a {@link GedcomFileWriter}. Deliberately
     * package-private so tests can access it but others can't alter it.
     */
    List<String> lines = new ArrayList<String>();

    /**
     * A list of things found during validation of the gedcom data prior to writing it. If the data cannot be written
     * due to an exception caused by failure to validate, this collection will describe the issues encountered.
     */
    public List<GedcomValidationFinding> validationFindings;

    /**
     * Whether or not to use autorepair in the validation step
     */
    public boolean autorepair = false;

    /**
     * Constructor
     *
     * @param gedcom
     *            the {@link Gedcom} structure to write out
     */
    public GedcomWriter(Gedcom gedcom) {
        this.gedcom = gedcom;
    }

    /**
     * Write the {@link Gedcom} data as a GEDCOM 5.5 file. Automatically fills in the value for the FILE tag in the HEAD
     * structure.
     *
     * @param file
     *            the {@link File} to write to
     * @throws IOException
     *             if there's a problem writing the data
     * @throws GedcomWriterException
     *             if the data is malformed and cannot be written
     */
    public void write(File file) throws IOException, GedcomWriterException {
        // Automatically replace the contents of the filename in the header
        gedcom.header.fileName = new StringWithCustomTags(file.getName());

        OutputStream o = new FileOutputStream(file);
        try {
            write(o);
            o.flush();
        } finally {
            o.close();
        }
    }

    /**
     * Write the {@link Gedcom} data in GEDCOM 5.5 format to an output stream. If the data is unicode, the data will be
     * written in little-endian format.
     *
     * @param out
     *            the output stream we're writing to
     * @throws GedcomWriterException
     *             if the data is malformed and cannot be written
     */
    public void write(OutputStream out) throws GedcomWriterException {
        write(out, true);
    }

    /**
     * Write the {@link Gedcom} data in GEDCOM 5.5 format to an output stream
     *
     * @param out
     *            the output stream we're writing to
     * @param littleEndianForUnicode
     *            if writing unicode, should the byte-order be little-endian? Ignored if output data encoding is not
     *            unicode.
     * @throws GedcomWriterException
     *             if the data is malformed and cannot be written; or if the data fails validation with one or more
     *             finding of severity ERROR (and validation is not suppressed - see
     *             {@link GedcomWriter#validationSuppressed})
     */
    public void write(OutputStream out, boolean littleEndianForUnicode) throws GedcomWriterException {
        if (!validationSuppressed) {
            GedcomValidator gv = new GedcomValidator(gedcom);
            gv.autorepair = autorepair;
            gv.validate();
            validationFindings = gv.findings;
            int numErrorFindings = 0;
            for (GedcomValidationFinding f : validationFindings) {
                if (f.severity == Severity.ERROR) {
                    numErrorFindings++;
                }
            }
            if (numErrorFindings > 0) {
                throw new GedcomWriterException("Cannot write file - " + numErrorFindings
                        + " error(s) found during validation.  Review the validation findings to determine root cause.");
            }
        }
        checkVersionCompatibility();
        emitHeader();
        emitSubmissionRecord();
        emitRecords();
        emitTrailer();
        emitCustomTags(gedcom.customTags);
        try {
            GedcomFileWriter gfw = new GedcomFileWriter(lines);
            gfw.useLittleEndianForUnicode = littleEndianForUnicode;
            gfw.write(out);
        } catch (IOException e) {
            throw new GedcomWriterException("Unable to write file", e);
        }
    }

    /**
     * Write the {@link Gedcom} data as a GEDCOM 5.5 file, with the supplied file name
     *
     * @param filename
     *            the name of the file to write
     * @throws IOException
     *             if there's a problem writing the data
     * @throws GedcomWriterException
     *             if the data is malformed and cannot be written
     */
    public void write(String filename) throws IOException, GedcomWriterException {
        File f = new File(filename);
        write(f);
    }

    /**
     * Checks that the gedcom version specified is compatible with the data in the model. Not a perfect exhaustive
     * check.
     *
     * @throws GedcomWriterException
     *             if data is detected that is incompatible with the selected version
     */
    private void checkVersionCompatibility() throws GedcomWriterException {

        if (gedcom.header.gedcomVersion == null) {
            // If there's not one specified, set up a default one that specifies
            // 5.5.1
            gedcom.header.gedcomVersion = new GedcomVersion();
        }
        if (SupportedVersion.V5_5.equals(gedcom.header.gedcomVersion.versionNumber)) {
            checkVersionCompatibility55();
        } else {
            checkVersionCompatibility551();
        }

    }

    /**
     * Check that the data is compatible with 5.5 style Gedcom files
     *
     * @throws GedcomWriterVersionDataMismatchException
     *             if a data point is detected that is incompatible with the 5.5 standard
     */
    private void checkVersionCompatibility55() throws GedcomWriterVersionDataMismatchException {
        // Now that we know if we're working with a 5.5.1 file or not, let's
        // check some data points
        if (gedcom.header.copyrightData.size() > 1) {
            throw new GedcomWriterVersionDataMismatchException(
                    "Gedcom version is 5.5, but has multi-line copyright data in header");
        }
        if (gedcom.header.characterSet != null && gedcom.header.characterSet.characterSetName != null
                && "UTF-8".equals(gedcom.header.characterSet.characterSetName.value)) {
            throw new GedcomWriterVersionDataMismatchException("Gedcom version is 5.5, but data is encoded using UTF-8");
        }
        if (gedcom.header.sourceSystem != null && gedcom.header.sourceSystem.corporation != null) {
            Corporation c = gedcom.header.sourceSystem.corporation;
            if (!c.wwwUrls.isEmpty()) {
                throw new GedcomWriterVersionDataMismatchException(
                        "Gedcom version is 5.5, but source system corporation has www urls");
            }
            if (!c.faxNumbers.isEmpty()) {
                throw new GedcomWriterVersionDataMismatchException(
                        "Gedcom version is 5.5, but source system corporation has fax numbers");
            }
            if (!c.emails.isEmpty()) {
                throw new GedcomWriterVersionDataMismatchException(
                        "Gedcom version is 5.5, but source system corporation has emails");
            }
        }
        for (Individual i : gedcom.individuals.values()) {
            if (!i.wwwUrls.isEmpty()) {
                throw new GedcomWriterVersionDataMismatchException("Gedcom version is 5.5, but Individual " + i.xref
                        + " has www urls");
            }
            if (!i.faxNumbers.isEmpty()) {
                throw new GedcomWriterVersionDataMismatchException("Gedcom version is 5.5, but Individual " + i.xref
                        + " has fax numbers");
            }
            if (!i.emails.isEmpty()) {
                throw new GedcomWriterVersionDataMismatchException("Gedcom version is 5.5, but Individual " + i.xref
                        + " has emails");
            }
            for (Event e : i.events) {
                if (!e.wwwUrls.isEmpty()) {
                    throw new GedcomWriterVersionDataMismatchException("Gedcom version is 5.5, but Individual "
                            + i.xref + " has www urls on an event");
                }
                if (!e.faxNumbers.isEmpty()) {
                    throw new GedcomWriterVersionDataMismatchException("Gedcom version is 5.5, but Individual "
                            + i.xref + " has fax numbers on an event");
                }
                if (!e.emails.isEmpty()) {
                    throw new GedcomWriterVersionDataMismatchException("Gedcom version is 5.5, but Individual "
                            + i.xref + " has emails on an event");
                }
            }
            for (IndividualAttribute a : i.attributes) {
                if (IndividualAttributeType.FACT.equals(a.type)) {
                    throw new GedcomWriterVersionDataMismatchException("Gedcom version is 5.5, but Individual "
                            + i.xref + " has a FACT attribute");
                }
            }
            for (FamilyChild fc : i.familiesWhereChild) {
                if (fc.status != null) {
                    throw new GedcomWriterVersionDataMismatchException("Gedcom version is 5.5, but Individual "
                            + i.xref + " is in a family with a status specified (a Gedcom 5.5.1 only feature)");
                }
            }
        }
        for (Submitter s : gedcom.submitters.values()) {
            if (!s.wwwUrls.isEmpty()) {
                throw new GedcomWriterVersionDataMismatchException("Gedcom version is 5.5, but Submitter " + s.xref
                        + " has www urls");
            }
            if (!s.faxNumbers.isEmpty()) {
                throw new GedcomWriterVersionDataMismatchException("Gedcom version is 5.5, but Submitter " + s.xref
                        + " has fax numbers");
            }
            if (!s.emails.isEmpty()) {
                throw new GedcomWriterVersionDataMismatchException("Gedcom version is 5.5, but Submitter " + s.xref
                        + " has emails");
            }
        }
        for (Repository r : gedcom.repositories.values()) {
            if (!r.wwwUrls.isEmpty()) {
                throw new GedcomWriterVersionDataMismatchException("Gedcom version is 5.5, but Repository " + r.xref
                        + " has www urls");
            }
            if (!r.faxNumbers.isEmpty()) {
                throw new GedcomWriterVersionDataMismatchException("Gedcom version is 5.5, but Repository " + r.xref
                        + " has fax numbers");
            }
            if (!r.emails.isEmpty()) {
                throw new GedcomWriterVersionDataMismatchException("Gedcom version is 5.5, but Repository " + r.xref
                        + " has emails");
            }
        }
    }

    /**
     * Check that the data is compatible with 5.5.1 style Gedcom files
     *
     * @throws GedcomWriterVersionDataMismatchException
     *             if a data point is detected that is incompatible with the 5.5.1 standard
     *
     */
    private void checkVersionCompatibility551() throws GedcomWriterVersionDataMismatchException {
        for (Multimedia m : gedcom.multimedia.values()) {
            if (!m.blob.isEmpty()) {
                throw new GedcomWriterVersionDataMismatchException("Gedcom version is 5.5.1, but multimedia item "
                        + m.xref + " contains BLOB data which is unsupported in 5.5.1");
            }
        }
    }

    /**
     * Write an address
     *
     * @param level
     *            the level at which we are writing
     * @param address
     *            the address
     */
    private void emitAddress(int level, Address address) {
        if (address == null) {
            return;
        }
        emitLinesOfText(level, "ADDR", address.lines);
        emitTagIfValueNotNull(level + 1, "ADR1", address.addr1);
        emitTagIfValueNotNull(level + 1, "ADR2", address.addr2);
        emitTagIfValueNotNull(level + 1, "CITY", address.city);
        emitTagIfValueNotNull(level + 1, "STAE", address.stateProvince);
        emitTagIfValueNotNull(level + 1, "POST", address.postalCode);
        emitTagIfValueNotNull(level + 1, "CTRY", address.country);
        emitCustomTags(address.customTags);
    }

    /**
     * Write a line out to the print writer, splitting if needed with CONC lines
     *
     * @param level
     *            the level at which we are recording
     * @param line
     *            the line to be written
     */
    private void emitAndSplit(int level, String line) {
        if (line.length() <= MAX_LINE_LENGTH) {
            lines.add(line);
        } else {
            // First part
            lines.add(line.substring(0, MAX_LINE_LENGTH));
            // Now a series of as many CONC lines as needed
            String remainder = line.substring(MAX_LINE_LENGTH);
            while (remainder.length() > 0) {
                if (remainder.length() > MAX_LINE_LENGTH) {
                    lines.add(level + 1 + " CONC " + remainder.substring(0, MAX_LINE_LENGTH));
                    remainder = remainder.substring(MAX_LINE_LENGTH);
                } else {
                    lines.add(level + 1 + " CONC " + remainder);
                    remainder = "";
                }
            }
        }
    }

    /**
     * Emit the person-to-person associations an individual was in - see ASSOCIATION_STRUCTURE in the GEDCOM spec.
     *
     * @param level
     *            the level at which to start recording
     * @param associations
     *            the list of associations
     * @throws GedcomWriterException
     *             if the data is malformed and cannot be written
     */
    private void emitAssociationStructures(int level, List<Association> associations) throws GedcomWriterException {
        for (Association a : associations) {
            emitTagWithRequiredValue(level, "ASSO", a.associatedEntityXref);
            emitTagWithRequiredValue(level + 1, "TYPE", a.associatedEntityType);
            emitTagWithRequiredValue(level + 1, "RELA", a.relationship);
            emitNotes(level + 1, a.notes);
            emitSourceCitations(level + 1, a.citations);
            emitCustomTags(a.customTags);
        }
    }

    /**
     * Emit a change date
     *
     * @param level
     *            the level at which we are emitting data
     * @param cd
     *            the change date
     * @throws GedcomWriterException
     *             if the data is malformed and cannot be written
     */
    private void emitChangeDate(int level, ChangeDate cd) throws GedcomWriterException {
        if (cd != null) {
            emitTag(level, "CHAN");
            emitTagWithRequiredValue(level + 1, "DATE", cd.date);
            emitTagIfValueNotNull(level + 2, "TIME", cd.time);
            emitNotes(level + 1, cd.notes);
            emitCustomTags(cd.customTags);
        }
    }

    /**
     * Emit links to all the families to which this individual was a child
     *
     * @param level
     *            the level in the hierarchy at which we are emitting
     * @param i
     *            the individual we're dealing with
     * @throws GedcomWriterException
     *             if the data is malformed and cannot be written
     */
    private void emitChildToFamilyLinks(int level, Individual i) throws GedcomWriterException {
        for (FamilyChild familyChild : i.familiesWhereChild) {
            if (familyChild == null) {
                throw new GedcomWriterException("Family to which " + i + " was a child was null");
            }
            if (familyChild.family == null) {
                throw new GedcomWriterException("Family to which " + i + " was a child had a null family reference");
            }
            emitTagWithRequiredValue(level, "FAMC", familyChild.family.xref);
            emitTagIfValueNotNull(level + 1, "PEDI", familyChild.pedigree);
            emitTagIfValueNotNull(level + 1, "STAT", familyChild.status);
            emitNotes(level + 1, familyChild.notes);
            emitCustomTags(i.customTags);
        }
    }

    /**
     * Emit a citation without a source
     *
     * @param level
     *            the level in the hierarchy at which we are emitting data
     * @param c
     *            the citation
     * @throws GedcomWriterException
     *             when the data is malformed and cannot be written
     */
    private void emitCitationWithoutSource(int level, AbstractCitation c) throws GedcomWriterException {
        CitationWithoutSource cws = (CitationWithoutSource) c;
        emitLinesOfText(level, "SOUR", cws.description);
        for (List<String> linesOfText : cws.textFromSource) {
            emitLinesOfText(level + 1, "TEXT", linesOfText);
        }
        emitNotes(level + 1, cws.notes);
        emitCustomTags(cws.customTags);
    }

    /**
     * Emit a citation with source
     *
     * @param level
     *            the level in the hierarchy at which we are emitting data
     * @param cws
     *            the citation with source
     * @throws GedcomWriterException
     *             when the data is malformed and cannot be written
     */
    private void emitCitationWithSource(int level, CitationWithSource cws) throws GedcomWriterException {
        Source source = cws.source;
        if (source == null || source.xref == null || source.xref.length() == 0) {
            throw new GedcomWriterException("Citation with source must have a source record with an xref/id");
        }
        emitTagWithRequiredValue(level, "SOUR", source.xref);
        emitTagIfValueNotNull(level + 1, "PAGE", cws.whereInSource);
        emitTagIfValueNotNull(level + 1, "EVEN", cws.eventCited);
        emitTagIfValueNotNull(level + 2, "ROLE", cws.roleInEvent);
        if (cws.data != null && !cws.data.isEmpty()) {
            emitTag(level + 1, "DATA");
            for (CitationData cd : cws.data) {
                emitTagIfValueNotNull(level + 2, "DATE", cd.entryDate);
                for (List<String> linesOfText : cd.sourceText) {
                    emitLinesOfText(level + 2, "TEXT", linesOfText);
                }
            }
        }
        emitTagIfValueNotNull(level + 1, "QUAY", cws.certainty);
        emitMultimediaLinks(level + 1, cws.multimedia);
        emitNotes(level + 1, cws.notes);
        emitCustomTags(cws.customTags);
    }

    /**
     * Emit the custom tags
     *
     * @param customTags
     *            the custom tags
     */
    private void emitCustomTags(List<StringTree> customTags) {
        for (StringTree st : customTags) {
            StringBuilder line = new StringBuilder(Integer.toString(st.level));
            line.append(" ");
            if (st.id != null && st.id.trim().length() > 0) {
                line.append(st.id).append(" ");
            }
            line.append(st.tag);
            if (st.value != null && st.value.trim().length() > 0) {
                line.append(" ").append(st.value);
            }
            emitCustomTags(st.children);
        }
    }

    /**
     * Emit a list of email addresses. New for GEDCOM 5.5.1
     *
     * @param l
     *            the level we are writing at
     * @param emails
     *            the list of email addresses
     * @throws GedcomWriterException
     *             if the data cannot be written
     */
    private void emitEmails(int l, List<StringWithCustomTags> emails) throws GedcomWriterException {
        for (StringWithCustomTags e : emails) {
            emitTagWithRequiredValue(l, "EMAIL", e);
        }
    }

    /**
     * Emit an event detail structure - see EVENT_DETAIL in the GEDCOM spec
     *
     * @param level
     *            the level at which we are emitting data
     * @param e
     *            the event being emitted
     * @throws GedcomWriterException
     *             if the data is malformed and cannot be written
     */
    private void emitEventDetail(int level, Event e) throws GedcomWriterException {
        emitTagIfValueNotNull(level, "TYPE", e.subType);
        emitTagIfValueNotNull(level, "DATE", e.date);
        if (e.place != null) {
            Place p = e.place;
            emitPlace(level, p);
        }
        emitAddress(level, e.address);
        emitTagIfValueNotNull(level, "AGE", e.age);
        emitTagIfValueNotNull(level, "AGNC", e.respAgency);
        emitTagIfValueNotNull(level, "CAUS", e.cause);
        emitTagIfValueNotNull(level, "RELI", e.religiousAffiliation);
        emitTagIfValueNotNull(level, "RESN", e.restrictionNotice);
        emitSourceCitations(level, e.citations);
        emitMultimediaLinks(level, e.multimedia);
        emitNotes(level, e.notes);
        emitCustomTags(e.customTags);
    }

    /**
     * Write out all the Families
     *
     * @throws GedcomWriterException
     *             if the data is malformed and cannot be written
     */
    private void emitFamilies() throws GedcomWriterException {
        for (Family f : gedcom.families.values()) {
            emitTag(0, f.xref, "FAM");
            for (FamilyEvent e : f.events) {
                emitFamilyEventStructure(1, e);
            }
            if (f.husband != null) {
                emitTagWithRequiredValue(1, "HUSB", f.husband.xref);
            }
            if (f.wife != null) {
                emitTagWithRequiredValue(1, "WIFE", f.wife.xref);
            }
            for (Individual i : f.children) {
                emitTagWithRequiredValue(1, "CHIL", i.xref);
            }
            emitTagIfValueNotNull(1, "NCHI", f.numChildren);
            for (Submitter s : f.submitters) {
                emitTagWithRequiredValue(1, "SUBM", s.xref);
            }
            for (LdsSpouseSealing s : f.ldsSpouseSealings) {
                emitLdsFamilyOrdinance(1, s);
            }
            emitTagIfValueNotNull(1, "RESN", f.restrictionNotice);
            emitSourceCitations(1, f.citations);
            emitMultimediaLinks(1, f.multimedia);
            emitNotes(1, f.notes);
            for (UserReference u : f.userReferences) {
                emitTagWithRequiredValue(1, "REFN", u.referenceNum);
                emitTagIfValueNotNull(2, "TYPE", u.type);
            }
            emitTagIfValueNotNull(1, "RIN", f.automatedRecordId);
            emitChangeDate(1, f.changeDate);
            emitCustomTags(f.customTags);
        }
    }

    /**
     * Emit a family event structure (see FAMILY_EVENT_STRUCTURE in the GEDCOM spec)
     *
     * @param level
     *            the level we're writing at
     * @param e
     *            the event
     * @throws GedcomWriterException
     *             if the data is malformed and cannot be written
     */
    private void emitFamilyEventStructure(int level, FamilyEvent e) throws GedcomWriterException {
        emitTagWithOptionalValue(level, e.type.tag, e.yNull);
        emitEventDetail(level + 1, e);
        if (e.husbandAge != null) {
            emitTag(level + 1, "HUSB");
            emitTagWithRequiredValue(level + 2, "AGE", e.husbandAge);
        }
        if (e.wifeAge != null) {
            emitTag(level + 1, "WIFE");
            emitTagWithRequiredValue(level + 2, "AGE", e.wifeAge);
        }
        emitCustomTags(e.customTags);
    }

    /**
     * Emit a list of Fax numbers. New for GEDCOM 5.5.1
     *
     * @param l
     *            the level to write at
     * @param faxNumbers
     *            the list of fax numbers to write
     * @throws GedcomWriterException
     *             if the data cannot be written
     */
    private void emitFaxNumbers(int l, List<StringWithCustomTags> faxNumbers) throws GedcomWriterException {
        for (StringWithCustomTags f : faxNumbers) {
            emitTagWithRequiredValue(l, "FAX", f);
        }
    }

    /**
     * Write the header record (see the HEADER structure in the GEDCOM standard)
     *
     * @throws GedcomWriterException
     *             if the data is malformed and cannot be written
     */
    private void emitHeader() throws GedcomWriterException {
        Header header = gedcom.header;
        if (header == null) {
            header = new Header();
        }
        lines.add("0 HEAD");
        emitSourceSystem(header.sourceSystem);
        emitTagIfValueNotNull(1, "DEST", header.destinationSystem);
        if (header.date != null) {
            emitTagIfValueNotNull(1, "DATE", header.date);
            emitTagIfValueNotNull(2, "TIME", header.time);
        }
        if (header.submitter != null) {
            emitTagWithRequiredValue(1, "SUBM", header.submitter.xref);
        }
        if (header.submission != null) {
            emitTagWithRequiredValue(1, "SUBN", header.submission.xref);
        }
        emitTagIfValueNotNull(1, "FILE", header.fileName);
        emitLinesOfText(1, "COPR", header.copyrightData);
        emitTag(1, "GEDC");
        emitTagWithRequiredValue(2, "VERS", header.gedcomVersion.versionNumber.toString());
        emitTagWithRequiredValue(2, "FORM", header.gedcomVersion.gedcomForm);
        emitTagWithRequiredValue(1, "CHAR", header.characterSet.characterSetName);
        emitTagIfValueNotNull(2, "VERS", header.characterSet.versionNum);
        emitTagIfValueNotNull(1, "LANG", header.language);
        if (header.placeHierarchy != null && header.placeHierarchy.value != null
                && header.placeHierarchy.value.length() > 0) {
            emitTag(1, "PLAC");
            emitTagWithRequiredValue(2, "FORM", header.placeHierarchy);
        }
        emitNoteLines(1, null, header.notes);
        emitCustomTags(header.customTags);
    }

    /**
     * Emit a collection of individual attributes
     *
     * @param level
     *            the level to start emitting at
     * @param attributes
     *            the attributes
     * @throws GedcomWriterException
     *             if the data is malformed and cannot be written
     */
    private void emitIndividualAttributes(int level, List<IndividualAttribute> attributes) throws GedcomWriterException {
        for (IndividualAttribute a : attributes) {
            emitTagWithOptionalValueAndCustomSubtags(level, a.type.tag, a.description);
            emitEventDetail(level + 1, a);
            emitAddress(level + 1, a.address);
            emitPhoneNumbers(level + 1, a.phoneNumbers);
            emitWwwUrls(level + 1, a.wwwUrls);
            emitFaxNumbers(level + 1, a.faxNumbers);
            emitEmails(level + 1, a.emails);
            emitCustomTags(a.customTags);
        }
    }

    /**
     * Emit a collection of individual events
     *
     * @param level
     *            the level to start emitting at
     * @param events
     *            the events
     * @throws GedcomWriterException
     *             if the data is malformed and cannot be written
     */
    private void emitIndividualEvents(int level, List<IndividualEvent> events) throws GedcomWriterException {
        for (IndividualEvent e : events) {
            emitTagWithOptionalValue(level, e.type.tag, e.yNull);
            emitEventDetail(level + 1, e);
            if (e.type == IndividualEventType.BIRTH || e.type == IndividualEventType.CHRISTENING) {
                if (e.family != null && e.family.family != null && e.family.family.xref != null) {
                    emitTagWithRequiredValue(level + 1, "FAMC", e.family.family.xref);
                }
            } else if (e.type == IndividualEventType.ADOPTION && e.family != null && e.family.family != null
                    && e.family.family.xref != null) {
                emitTagWithRequiredValue(level + 1, "FAMC", e.family.family.xref);
                emitTagIfValueNotNull(level + 2, "ADOP", e.family.adoptedBy);
            }
            emitCustomTags(e.customTags);
        }
    }

    /**
     * Write out all the individuals at the root level
     *
     * @throws GedcomWriterException
     *             if the data is malformed and cannot be written
     */
    private void emitIndividuals() throws GedcomWriterException {
        for (Individual i : gedcom.individuals.values()) {
            emitTag(0, i.xref, "INDI");
            emitTagIfValueNotNull(1, "RESN", i.restrictionNotice);
            emitPersonalNames(1, i.names);
            emitTagIfValueNotNull(1, "SEX", i.sex);
            emitIndividualEvents(1, i.events);
            emitIndividualAttributes(1, i.attributes);
            emitLdsIndividualOrdinances(1, i.ldsIndividualOrdinances);
            emitChildToFamilyLinks(1, i);
            emitSpouseInFamilyLinks(1, i);
            for (Submitter s : i.submitters) {
                emitTagWithRequiredValue(1, "SUBM", s.xref);
            }
            emitAssociationStructures(1, i.associations);
            for (StringWithCustomTags s : i.aliases) {
                emitTagWithRequiredValue(1, "ALIA", s);
            }
            for (Submitter s : i.ancestorInterest) {
                emitTagWithRequiredValue(1, "ANCI", s.xref);
            }
            for (Submitter s : i.descendantInterest) {
                emitTagWithRequiredValue(1, "DESI", s.xref);
            }
            emitSourceCitations(1, i.citations);
            emitMultimediaLinks(1, i.multimedia);
            emitNotes(1, i.notes);
            emitTagIfValueNotNull(1, "RFN", i.permanentRecFileNumber);
            emitTagIfValueNotNull(1, "AFN", i.ancestralFileNumber);
            for (UserReference u : i.userReferences) {
                emitTagWithRequiredValue(1, "REFN", u.referenceNum);
                emitTagIfValueNotNull(2, "TYPE", u.type);
            }
            emitTagIfValueNotNull(1, "RIN", i.recIdNumber);
            emitChangeDate(1, i.changeDate);
            emitCustomTags(i.customTags);
        }
    }

    /**
     * Emit the LDS spouse sealing information
     *
     * @param level
     *            the level we're writing at
     * @param sealings
     *            the {@link LdsSpouseSealing} structure
     * @throws GedcomWriterException
     *             if the data is malformed and cannot be written
     */
    private void emitLdsFamilyOrdinance(int level, LdsSpouseSealing sealings) throws GedcomWriterException {
        emitTag(level, "SLGS");
        emitTagIfValueNotNull(level + 1, "STAT", sealings.status);
        emitTagIfValueNotNull(level + 1, "DATE", sealings.date);
        emitTagIfValueNotNull(level + 1, "TEMP", sealings.temple);
        emitTagIfValueNotNull(level + 1, "PLAC", sealings.place);
        emitSourceCitations(level + 1, sealings.citations);
        emitNotes(level + 1, sealings.notes);
        emitCustomTags(sealings.customTags);
    }

    /**
     * Emit the LDS individual ordinances
     *
     * @param level
     *            the level in the hierarchy we are processing
     * @param ldsIndividualOrdinances
     *            the list of LDS individual ordinances to emit
     * @throws GedcomWriterException
     *             if the data is malformed and cannot be written
     */
    private void emitLdsIndividualOrdinances(int level, List<LdsIndividualOrdinance> ldsIndividualOrdinances)
            throws GedcomWriterException {
        for (LdsIndividualOrdinance o : ldsIndividualOrdinances) {
            emitTagWithOptionalValue(level, o.type.tag, o.yNull);
            emitTagIfValueNotNull(level + 1, "STAT", o.status);
            emitTagIfValueNotNull(level + 1, "DATE", o.date);
            emitTagIfValueNotNull(level + 1, "TEMP", o.temple);
            emitTagIfValueNotNull(level + 1, "PLAC", o.place);
            if (o.type == LdsIndividualOrdinanceType.CHILD_SEALING) {
                if (o.familyWhereChild == null) {
                    throw new GedcomWriterException(
                            "LDS Ordinance info for a child sealing had no reference to a family");
                }
                if (o.familyWhereChild.family == null) {
                    throw new GedcomWriterException(
                            "LDS Ordinance info for a child sealing had familyChild object with a null reference to a family");
                }
                emitTagWithRequiredValue(level + 1, "FAMC", o.familyWhereChild.family.xref);
            }
            emitSourceCitations(level + 1, o.citations);
            emitNotes(level + 1, o.notes);
            emitCustomTags(o.customTags);
        }
    }

    /**
     * Convenience method for emitting lines of text when there is no xref, so you don't have to pass null all the time
     *
     * @param level
     *            the level we are starting at. Continuation lines will be one level deeper than this value
     * @param startingTag
     *            the tag to use for the first line of the text. All subsequent lines will be "CONT" lines.
     * @param linesOfText
     *            the lines of text
     */
    private void emitLinesOfText(int level, String startingTag, List<String> linesOfText) {
        emitLinesOfText(level, null, startingTag, linesOfText);
    }

    /**
     * Emit a multi-line text value.
     *
     * @param level
     *            the level we are starting at. Continuation lines will be one level deeper than this value
     * @param startingTag
     *            the tag to use for the first line of the text. All subsequent lines will be "CONT" lines.
     * @param xref
     *            the xref of the item with lines of text
     * @param linesOfText
     *            the lines of text
     */
    private void emitLinesOfText(int level, String xref, String startingTag, List<String> linesOfText) {
        int lineNum = 0;
        for (String l : linesOfText) {
            StringBuilder line = new StringBuilder();
            if (lineNum == 0) {
                line.append(level).append(" ");
                if (xref != null && xref.length() > 0) {
                    line.append(xref).append(" ");
                }
                line.append(startingTag).append(" ").append(l);
            } else {
                line.append(level + 1).append(" ");
                if (xref != null && xref.length() > 0) {
                    line.append(xref).append(" ");
                }
                line.append("CONT ").append(l);
            }
            lineNum++;
            emitAndSplit(level, line.toString());
        }
    }

    /**
     * Write out all the embedded multimedia objects in GEDCOM 5.5 format
     *
     * @throws GedcomWriterException
     *             if the data is malformed and cannot be written
     */
    private void emitMultimedia55() throws GedcomWriterException {
        for (Multimedia m : gedcom.multimedia.values()) {
            emitTag(0, m.xref, "OBJE");
            emitTagWithRequiredValue(1, "FORM", m.embeddedMediaFormat);
            emitTagIfValueNotNull(1, "TITL", m.embeddedTitle);
            emitNotes(1, m.notes);
            emitTag(1, "BLOB");
            for (String b : m.blob) {
                emitTagWithRequiredValue(2, "CONT", b);
            }
            if (m.continuedObject != null && m.continuedObject.xref != null) {
                emitTagWithRequiredValue(1, "OBJE", m.continuedObject.xref);
            }
            for (UserReference u : m.userReferences) {
                emitTagWithRequiredValue(1, "REFN", u.referenceNum);
                emitTagIfValueNotNull(2, "TYPE", u.type);
            }
            emitTagIfValueNotNull(1, "RIN", m.recIdNumber);
            emitChangeDate(1, m.changeDate);
            if (!m.fileReferences.isEmpty()) {
                throw new GedcomWriterVersionDataMismatchException(
                        "GEDCOM version is 5.5, but found file references in multimedia object " + m.xref
                                + " which are not allowed until GEDCOM 5.5.1");
            }
            emitCustomTags(m.customTags);
        }
    }

    /**
     * Write out all the embedded multimedia objects in GEDCOM 5.5.1 format
     *
     * @throws GedcomWriterException
     *             if the data is malformed and cannot be written
     */
    private void emitMultimedia551() throws GedcomWriterException {
        for (Multimedia m : gedcom.multimedia.values()) {
            emitTag(0, m.xref, "OBJE");
            for (FileReference fr : m.fileReferences) {
                emitTagWithRequiredValue(1, "FILE", fr.referenceToFile);
                emitTagWithRequiredValue(2, "FORM", fr.format);
                emitTagIfValueNotNull(3, "TYPE", fr.mediaType);
                emitTagIfValueNotNull(2, "TITL", fr.title);
            }
            for (UserReference u : m.userReferences) {
                emitTagWithRequiredValue(1, "REFN", u.referenceNum);
                emitTagIfValueNotNull(2, "TYPE", u.type);
            }
            emitTagIfValueNotNull(1, "RIN", m.recIdNumber);
            emitNotes(1, m.notes);
            emitChangeDate(1, m.changeDate);
            emitCustomTags(m.customTags);
            if (!m.blob.isEmpty()) {
                throw new GedcomWriterVersionDataMismatchException(
                        "GEDCOM version is 5.5.1, but BLOB data on multimedia item " + m.xref
                                + " was found.  This is only allowed in GEDCOM 5.5");
            }
            if (m.continuedObject != null) {
                throw new GedcomWriterVersionDataMismatchException(
                        "GEDCOM version is 5.5.1, but BLOB continuation data on multimedia item " + m.xref
                                + " was found.  This is only allowed in GEDCOM 5.5");
            }
            if (m.embeddedMediaFormat != null) {
                throw new GedcomWriterVersionDataMismatchException(
                        "GEDCOM version is 5.5.1, but format on multimedia item " + m.xref
                                + " was found.  This is only allowed in GEDCOM 5.5");
            }
            if (m.embeddedTitle != null) {
                throw new GedcomWriterVersionDataMismatchException(
                        "GEDCOM version is 5.5.1, but title on multimedia item " + m.xref
                                + " was found.  This is only allowed in GEDCOM 5.5");
            }
        }
    }

    /**
     * Emit a list of multimedia links
     *
     * @param level
     *            the level in the hierarchy we are writing at
     * @param multimedia
     *            the {@link List} of {@link Multimedia} objects
     * @throws GedcomWriterException
     *             if the data is malformed and cannot be written
     */
    private void emitMultimediaLinks(int level, List<Multimedia> multimedia) throws GedcomWriterException {
        if (multimedia == null) {
            return;
        }
        for (Multimedia m : multimedia) {
            if (m.xref == null) {
                // Link to referenced form
                if (g55()) {
                    // GEDCOM 5.5 format
                    emitTag(level, "OBJE");
                    if (m.fileReferences.size() > 1) {
                        throw new GedcomWriterVersionDataMismatchException(
                                "GEDCOM version is 5.5, but multimedia link references "
                                        + "multiple files, which is only allowed in GEDCOM 5.5.1");
                    }
                    if (m.fileReferences.size() == 1) {
                        FileReference fr = m.fileReferences.get(0);
                        if (fr.format == null) {
                            emitTagWithRequiredValue(level + 1, "FORM", m.embeddedMediaFormat);
                        } else {
                            emitTagWithRequiredValue(level + 1, "FORM", fr.format);
                        }
                        emitTagIfValueNotNull(level + 1, "TITL", m.embeddedTitle);
                        emitTagWithRequiredValue(level + 1, "FILE", fr.referenceToFile);
                    } else {
                        emitTagWithRequiredValue(level + 1, "FORM", m.embeddedMediaFormat);
                        emitTagIfValueNotNull(level + 1, "TITL", m.embeddedTitle);
                    }
                    emitNotes(level + 1, m.notes);
                } else {
                    // GEDCOM 5.5.1 format
                    for (FileReference fr : m.fileReferences) {
                        emitTagWithRequiredValue(level + 1, "FILE", fr.referenceToFile);
                        emitTagIfValueNotNull(level + 2, "FORM", fr.format);
                        emitTagIfValueNotNull(level + 3, "MEDI", fr.mediaType);
                        emitTagIfValueNotNull(level + 1, "TITL", fr.title);
                    }
                    if (!m.notes.isEmpty()) {
                        throw new GedcomWriterVersionDataMismatchException(
                                "GEDCOM version is 5.5.1, but multimedia link has notes which are no longer allowed in 5.5");
                    }
                }
            } else {
                // Link to the embedded form
                emitTagWithRequiredValue(level, "OBJE", m.xref);
            }
            emitCustomTags(m.customTags);
        }
    }

    /**
     * Emit a note structure (possibly multi-line)
     *
     * @param level
     *            the level in the hierarchy we are writing at
     * @param note
     *            the note structure
     * @throws GedcomWriterException
     *             if the data is malformed and cannot be written
     */
    private void emitNote(int level, Note note) throws GedcomWriterException {
        if (level > 0 && note.xref != null) {
            emitTagWithRequiredValue(level, "NOTE", note.xref);
            return;
        }
        emitNoteLines(level, note.xref, note.lines);
        emitSourceCitations(level + 1, note.citations);
        for (UserReference u : note.userReferences) {
            emitTagWithRequiredValue(level + 1, "REFN", u.referenceNum);
            emitTagIfValueNotNull(level + 2, "TYPE", u.type);
        }
        emitTagIfValueNotNull(level + 1, "RIN", note.recIdNumber);
        emitChangeDate(level + 1, note.changeDate);
        emitCustomTags(note.customTags);
    }

    /**
     * Emit line(s) of a note
     *
     * @param level
     *            the level in the hierarchy we are writing at
     * @param xref
     *            the xref of the note being written
     * @param noteLines
     *            the Notes text
     */
    private void emitNoteLines(int level, String xref, List<String> noteLines) {
        emitLinesOfText(level, xref, "NOTE", noteLines);
    }

    /**
     * Emit a list of note structures
     *
     * @param level
     *            the level in the hierarchy we are writing at
     * @param notes
     *            a list of {@link Note} structures
     * @throws GedcomWriterException
     *             if the data is malformed and cannot be written
     */
    private void emitNotes(int level, List<Note> notes) throws GedcomWriterException {
        for (Note n : notes) {
            emitNote(level, n);
            emitCustomTags(n.customTags);
        }
    }

    /**
     * Emit a list of personal names for an individual
     *
     * @param level
     *            the level to start emitting at
     * @param names
     *            the names
     * @throws GedcomWriterException
     *             if the data is malformed and cannot be written
     */
    private void emitPersonalNames(int level, List<PersonalName> names) throws GedcomWriterException {
        for (PersonalName n : names) {
            emitTagWithOptionalValue(level, "NAME", n.basic);
            emitTagIfValueNotNull(level + 1, "NPFX", n.prefix);
            emitTagIfValueNotNull(level + 1, "GIVN", n.givenName);
            emitTagIfValueNotNull(level + 1, "NICK", n.nickname);
            emitTagIfValueNotNull(level + 1, "SPFX", n.surnamePrefix);
            emitTagIfValueNotNull(level + 1, "SURN", n.surname);
            emitTagIfValueNotNull(level + 1, "NSFX", n.suffix);
            for (PersonalNameVariation pnv : n.romanized) {
                emitPersonalNameVariation(level + 1, "ROMN", pnv);
            }
            for (PersonalNameVariation pnv : n.phonetic) {
                emitPersonalNameVariation(level + 1, "FONE", pnv);
            }
            emitSourceCitations(level + 1, n.citations);
            emitNotes(level + 1, n.notes);
            emitCustomTags(n.customTags);
        }
    }

    /**
     * Emit a personal name variation - either romanized or phonetic
     *
     * @param level
     *            - the level we are writing at
     * @param variationTag
     *            the tag for the type of variation - should be "ROMN" for romanized, or "FONE" for phonetic
     * @param pnv
     *            - the personal name variation we are writing
     * @throws GedcomWriterException
     *             if the data is malformed and cannot be written
     */
    private void emitPersonalNameVariation(int level, String variationTag, PersonalNameVariation pnv)
            throws GedcomWriterException {
        emitTagWithRequiredValue(level, variationTag, pnv.variation);
        emitTagIfValueNotNull(level + 1, "NPFX", pnv.prefix);
        emitTagIfValueNotNull(level + 1, "GIVN", pnv.givenName);
        emitTagIfValueNotNull(level + 1, "NICK", pnv.nickname);
        emitTagIfValueNotNull(level + 1, "SPFX", pnv.surnamePrefix);
        emitTagIfValueNotNull(level + 1, "SURN", pnv.surname);
        emitTagIfValueNotNull(level + 1, "NSFX", pnv.suffix);
        emitSourceCitations(level + 1, pnv.citations);
        emitNotes(level + 1, pnv.notes);
        emitCustomTags(pnv.customTags);
    }

    /**
     * Write out a list of phone numbers
     *
     * @param level
     *            the level in the hierarchy we are writing at
     * @param phoneNumbers
     *            a list of phone numbers
     */
    private void emitPhoneNumbers(int level, List<StringWithCustomTags> phoneNumbers) {
        for (StringWithCustomTags ph : phoneNumbers) {
            emitTagIfValueNotNull(level, "PHON", ph);
        }
    }

    /**
     * Emit a place.
     *
     * @param level
     *            the level at which we are emitting data
     * @param p
     *            the place being emitted
     * @throws GedcomWriterException
     *             if the data is malformed and cannot be written
     */
    private void emitPlace(int level, Place p) throws GedcomWriterException {
        emitTagWithOptionalValue(level, "PLAC", p.placeName);
        emitTagIfValueNotNull(level + 1, "FORM", p.placeFormat);
        emitSourceCitations(level + 1, p.citations);
        emitNotes(level + 1, p.notes);
        for (NameVariation nv : p.romanized) {
            if (g55()) {
                throw new GedcomWriterVersionDataMismatchException(
                        "GEDCOM version is 5.5, but romanized variation was specified on place " + p.placeName
                                + ", which is only allowed in GEDCOM 5.5.1");
            }
            emitTagWithRequiredValue(level + 1, "ROMN", nv.variation);
            emitTagIfValueNotNull(level + 2, "TYPE", nv.variationType);
        }
        for (NameVariation nv : p.phonetic) {
            if (g55()) {
                throw new GedcomWriterVersionDataMismatchException(
                        "GEDCOM version is 5.5, but phonetic variation was specified on place " + p.placeName
                                + ", which is only allowed in GEDCOM 5.5.1");
            }
            emitTagWithRequiredValue(level + 1, "FONE", nv.variation);
            emitTagIfValueNotNull(level + 2, "TYPE", nv.variationType);
        }
        if (p.latitude != null || p.longitude != null) {
            emitTag(level + 1, "MAP");
            emitTagWithRequiredValue(level + 2, "LAT", p.latitude);
            emitTagWithRequiredValue(level + 2, "LONG", p.longitude);
            if (g55()) {
                throw new GedcomWriterVersionDataMismatchException(
                        "GEDCOM version is 5.5, but map coordinates were specified on place " + p.placeName
                                + ", which is only allowed in GEDCOM 5.5.1");
            }
        }
        emitCustomTags(p.customTags);
    }

    /**
     * Write the records (see the RECORD structure in the GEDCOM standard)
     *
     * @throws GedcomWriterException
     *             if the data is malformed and cannot be written
     */
    private void emitRecords() throws GedcomWriterException {
        emitIndividuals();
        emitFamilies();
        if (g55()) {
            emitMultimedia55();
        } else {
            emitMultimedia551();
        }
        emitNotes(0, new ArrayList<Note>(gedcom.notes.values()));
        emitRepositories();
        emitSources();
        emitSubmitter();
    }

    /**
     * Write out all the repositories (see REPOSITORY_RECORD in the Gedcom spec)
     *
     * @throws GedcomWriterException
     *             if the data being written is malformed
     */
    private void emitRepositories() throws GedcomWriterException {
        for (Repository r : gedcom.repositories.values()) {
            emitTag(0, r.xref, "REPO");
            emitTagIfValueNotNull(1, "NAME", r.name);
            emitAddress(1, r.address);
            emitNotes(1, r.notes);
            for (UserReference u : r.userReferences) {
                emitTagWithRequiredValue(1, "REFN", u.referenceNum);
                emitTagIfValueNotNull(2, "TYPE", u.type);
            }
            emitTagIfValueNotNull(1, "RIN", r.recIdNumber);
            emitPhoneNumbers(1, r.phoneNumbers);
            emitWwwUrls(1, r.wwwUrls);
            emitFaxNumbers(1, r.faxNumbers);
            emitEmails(1, r.emails);
            emitChangeDate(1, r.changeDate);
            emitCustomTags(r.customTags);
        }
    }

    /**
     * Write out a repository citation (see SOURCE_REPOSITORY_CITATION in the gedcom spec)
     *
     * @param level
     *            the level we're writing at
     * @param repositoryCitation
     *            the repository citation to write out
     * @throws GedcomWriterException
     *             if the repository citation passed in has a null repository reference
     */
    private void emitRepositoryCitation(int level, RepositoryCitation repositoryCitation) throws GedcomWriterException {
        if (repositoryCitation != null) {
            if (repositoryCitation.repositoryXref == null) {
                throw new GedcomWriterException("Repository Citation has null repository reference");
            }
            emitTagWithRequiredValue(level, "REPO", repositoryCitation.repositoryXref);
            emitNotes(level + 1, repositoryCitation.notes);
            for (SourceCallNumber scn : repositoryCitation.callNumbers) {
                emitTagWithRequiredValue(level + 1, "CALN", scn.callNumber);
                emitTagIfValueNotNull(level + 2, "MEDI", scn.mediaType);
            }
            emitCustomTags(repositoryCitation.customTags);
        }

    }

    /**
     * Write out a list of source citations
     *
     * @param level
     *            the level in the hierarchy we are writing at
     * @param citations
     *            the source citations
     * @throws GedcomWriterException
     *             if the data is malformed and cannot be written
     */
    private void emitSourceCitations(int level, List<AbstractCitation> citations) throws GedcomWriterException {
        if (citations == null) {
            return;
        }
        for (AbstractCitation c : citations) {
            if (c instanceof CitationWithoutSource) {
                emitCitationWithoutSource(level, c);
            } else if (c instanceof CitationWithSource) {
                emitCitationWithSource(level, (CitationWithSource) c);
            }
        }

    }

    /**
     * Write out all the sources (see SOURCE_RECORD in the Gedcom spec)
     *
     * @throws GedcomWriterException
     *             if the data being written is malformed
     */
    private void emitSources() throws GedcomWriterException {
        for (Source s : gedcom.sources.values()) {
            emitTag(0, s.xref, "SOUR");
            SourceData d = s.data;
            if (d != null) {
                emitTag(1, "DATA");
                for (EventRecorded e : d.eventsRecorded) {
                    emitTagWithOptionalValue(2, "EVEN", e.eventType);
                    emitTagIfValueNotNull(3, "DATE", e.datePeriod);
                    emitTagIfValueNotNull(3, "PLAC", e.jurisdiction);
                }
                emitTagIfValueNotNull(2, "AGNC", d.respAgency);
                emitNotes(2, d.notes);
            }
            emitLinesOfText(1, "AUTH", s.originatorsAuthors);
            emitLinesOfText(1, "TITL", s.title);
            emitTagIfValueNotNull(1, "ABBR", s.sourceFiledBy);
            emitLinesOfText(1, "PUBL", s.publicationFacts);
            emitLinesOfText(1, "TEXT", s.sourceText);
            emitRepositoryCitation(1, s.repositoryCitation);
            emitMultimediaLinks(1, s.multimedia);
            emitNotes(1, s.notes);
            for (UserReference u : s.userReferences) {
                emitTagWithRequiredValue(1, "REFN", u.referenceNum);
                emitTagIfValueNotNull(2, "TYPE", u.type);
            }
            emitTagIfValueNotNull(1, "RIN", s.recIdNumber);
            emitChangeDate(1, s.changeDate);
            emitCustomTags(s.customTags);
        }
    }

    /**
     * Write a source system structure (see APPROVED_SYSTEM_ID in the GEDCOM spec)
     *
     * @param sourceSystem
     *            the source system
     * @throws GedcomWriterException
     *             if data is malformed and cannot be written
     */
    private void emitSourceSystem(SourceSystem sourceSystem) throws GedcomWriterException {
        if (sourceSystem == null) {
            return;
        }
        emitTagWithRequiredValue(1, "SOUR", sourceSystem.systemId);
        emitTagIfValueNotNull(2, "VERS", sourceSystem.versionNum);
        emitTagIfValueNotNull(2, "NAME", sourceSystem.productName);
        Corporation corporation = sourceSystem.corporation;
        if (corporation != null) {
            emitTagWithOptionalValue(2, "CORP", corporation.businessName);
            emitAddress(3, corporation.address);
            emitPhoneNumbers(3, corporation.phoneNumbers);
            emitFaxNumbers(3, corporation.faxNumbers);
            emitWwwUrls(3, corporation.wwwUrls);
            emitEmails(3, corporation.emails);
        }
        HeaderSourceData sourceData = sourceSystem.sourceData;
        if (sourceData != null) {
            emitTagIfValueNotNull(2, "DATA", sourceData.name);
            emitTagIfValueNotNull(3, "DATE", sourceData.publishDate);
            emitTagIfValueNotNull(3, "COPR", sourceData.copyright);
        }
        emitCustomTags(sourceSystem.customTags);
    }

    /**
     * Emit links to all the families to which the supplied individual was a spouse
     *
     * @param level
     *            the level in the hierarchy at which we are emitting
     * @param i
     *            the individual we are dealing with
     * @throws GedcomWriterException
     *             if data is malformed and cannot be written
     */
    private void emitSpouseInFamilyLinks(int level, Individual i) throws GedcomWriterException {
        for (FamilySpouse familySpouse : i.familiesWhereSpouse) {
            if (familySpouse == null) {
                throw new GedcomWriterException("Family in which " + i + " was a spouse was null");
            }
            if (familySpouse.family == null) {
                throw new GedcomWriterException("Family in which " + i + " was a spouse had a null family reference");
            }
            emitTagWithRequiredValue(level, "FAMS", familySpouse.family.xref);
            emitNotes(level + 1, familySpouse.notes);
            emitCustomTags(familySpouse.customTags);
        }
    }

    /**
     * Write the SUBMISSION_RECORD
     *
     * @throws GedcomWriterException
     *             if the data is malformed and cannot be written
     */
    private void emitSubmissionRecord() throws GedcomWriterException {
        Submission s = gedcom.submission;
        if (s == null) {
            return;
        }
        emitTag(0, s.xref, "SUBN");
        if (s.submitter != null) {
            emitTagWithOptionalValue(1, "SUBM", s.submitter.xref);
        }
        emitTagIfValueNotNull(1, "FAMF", s.nameOfFamilyFile);
        emitTagIfValueNotNull(1, "TEMP", s.templeCode);
        emitTagIfValueNotNull(1, "ANCE", s.ancestorsCount);
        emitTagIfValueNotNull(1, "DESC", s.descendantsCount);
        emitTagIfValueNotNull(1, "ORDI", s.ordinanceProcessFlag);
        emitTagIfValueNotNull(1, "RIN", s.recIdNumber);
        emitCustomTags(s.customTags);
    }

    /**
     * Write out the submitter record
     *
     * @throws GedcomWriterException
     *             if the data is malformed and cannot be written
     */
    private void emitSubmitter() throws GedcomWriterException {
        for (Submitter s : gedcom.submitters.values()) {
            emitTag(0, s.xref, "SUBM");
            emitTagWithOptionalValueAndCustomSubtags(1, "NAME", s.name);
            emitAddress(1, s.address);
            emitMultimediaLinks(1, s.multimedia);
            for (StringWithCustomTags l : s.languagePref) {
                emitTagWithRequiredValue(1, "LANG", l);
            }
            emitPhoneNumbers(1, s.phoneNumbers);
            emitWwwUrls(1, s.wwwUrls);
            emitFaxNumbers(1, s.faxNumbers);
            emitEmails(1, s.emails);
            emitTagIfValueNotNull(1, "RFN", s.regFileNumber);
            emitTagIfValueNotNull(1, "RIN", s.recIdNumber);
            emitChangeDate(1, s.changeDate);
            emitCustomTags(s.customTags);
        }
    }

    /**
     * Write a line with a tag, with no value following the tag
     *
     * @param level
     *            the level within the file hierarchy
     * @param tag
     *            the tag for the line of the file
     */
    private void emitTag(int level, String tag) {
        lines.add(level + " " + tag);
    }

    /**
     * Write a line with a tag, with no value following the tag
     *
     * @param level
     *            the level within the file hierarchy
     * @param xref
     *            the xref of the item being written, if any
     * @param tag
     *            the tag for the line of the file
     */
    private void emitTag(int level, String xref, String tag) {
        StringBuilder line = new StringBuilder(Integer.toString(level));
        if (xref != null && xref.length() > 0) {
            line.append(" ").append(xref);
        }
        line.append(" ").append(tag);
        lines.add(line.toString());
    }

    /**
     * Write a line if the value is non-null
     *
     * @param level
     *            the level within the file hierarchy
     * @param tag
     *            the tag for the line of the file
     * @param value
     *            the value to write to the right of the tag
     */
    private void emitTagIfValueNotNull(int level, String tag, Object value) {
        emitTagIfValueNotNull(level, null, tag, value);
    }

    /**
     * Write a line if the value is non-null
     *
     * @param level
     *            the level within the file hierarchy
     * @param xref
     *            the xref for the item, if any
     * @param tag
     *            the tag for the line of the file
     * @param value
     *            the value to write to the right of the tag
     */
    private void emitTagIfValueNotNull(int level, String xref, String tag, Object value) {
        if (value != null) {
            StringBuilder line = new StringBuilder();
            line.append(level);
            if (xref != null && xref.length() > 0) {
                line.append(" ").append(xref);
            }
            line.append(" ").append(tag).append(" ").append(value);
            emitAndSplit(level, line.toString());
        }
    }

    /**
     * Write a line and tag, with an optional value for the tag
     *
     * @param level
     *            the level within the file hierarchy
     * @param tag
     *            the tag for the line of the file
     * @param value
     *            the value to write to the right of the tag
     * @throws GedcomWriterException
     *             if the value is null or blank (which never happens, because we check for it)
     */
    private void emitTagWithOptionalValue(int level, String tag, String value) throws GedcomWriterException {
        StringBuilder line = new StringBuilder(Integer.toString(level));
        line.append(" ").append(tag);
        if (value != null) {
            line.append(" ").append(value);
        }
        lines.add(line.toString());
    }

    /**
     * Write a line and tag, with an optional value for the tag
     *
     * @param level
     *            the level within the file hierarchy
     * @param tag
     *            the tag for the line of the file
     * @param valueToRightOfTag
     *            the value to write to the right of the tag
     * @throws GedcomWriterException
     *             if the value is null or blank (which never happens, because we check for it)
     */
    private void emitTagWithOptionalValueAndCustomSubtags(int level, String tag, StringWithCustomTags valueToRightOfTag)
            throws GedcomWriterException {
        StringBuilder line = new StringBuilder(Integer.toString(level));
        line.append(" ").append(tag);
        if (valueToRightOfTag != null && valueToRightOfTag.value != null) {
            line.append(" ").append(valueToRightOfTag);
        }
        lines.add(line.toString());
        if (valueToRightOfTag != null) {
            emitCustomTags(valueToRightOfTag.customTags);
        }
    }

    /**
     * Write a line and tag, with an optional value for the tag
     *
     * @param level
     *            the level within the file hierarchy
     * @param tag
     *            the tag for the line of the file
     * @param value
     *            the value to write to the right of the tag
     * @throws GedcomWriterException
     *             if the value is null or blank
     */
    private void emitTagWithRequiredValue(int level, String tag, String value) throws GedcomWriterException {
        emitTagWithRequiredValue(level, null, tag, new StringWithCustomTags(value));
    }

    /**
     * Write a line and tag, with an optional value for the tag
     *
     * @param level
     *            the level within the file hierarchy
     * @param tag
     *            the tag for the line of the file
     * @param e
     *            the value to write to the right of the tag
     * @param xref
     *            the xref of the item being emitted
     * @throws GedcomWriterException
     *             if the value is null or blank
     */
    private void emitTagWithRequiredValue(int level, String xref, String tag, StringWithCustomTags e)
            throws GedcomWriterException {
        if (e == null || e.value == null || e.value.trim().length() == 0) {
            throw new GedcomWriterException("Required value for tag " + tag + " at level " + level
                    + " was null or blank");
        }
        StringBuilder line = new StringBuilder(Integer.toString(level));
        if (xref != null && xref.length() > 0) {
            line.append(" ").append(xref);
        }
        line.append(" ").append(tag).append(" ").append(e);
        lines.add(line.toString());
        emitCustomTags(e.customTags);
    }

    /**
     * Write a line and tag, with an optional value for the tag
     *
     * @param level
     *            the level within the file hierarchy
     * @param tag
     *            the tag for the line of the file
     * @param value
     *            the value to write to the right of the tag
     * @throws GedcomWriterException
     *             if the value is null or blank
     */
    private void emitTagWithRequiredValue(int level, String tag, StringWithCustomTags value)
            throws GedcomWriterException {
        emitTagWithRequiredValue(level, null, tag, value);
    }

    /**
     * Write out the trailer record
     */
    private void emitTrailer() {
        lines.add("0 TRLR");
    }

    /**
     * Emit a list of WWW URLs. New for GEDCOM 5.5.1
     *
     * @param l
     *            the level to write at
     * @param wwwUrls
     *            the list of URLs
     * @throws GedcomWriterException
     *             if the data cannot be written
     */
    private void emitWwwUrls(int l, List<StringWithCustomTags> wwwUrls) throws GedcomWriterException {
        for (StringWithCustomTags w : wwwUrls) {
            emitTagWithRequiredValue(l, "WWW", w);
        }
    }

    /**
     * Returns true if and only if the Gedcom data says it is for the 5.5 standard.
     *
     * @return true if and only if the Gedcom data says it is for the 5.5 standard.
     */
    private boolean g55() {
        return gedcom != null && gedcom.header != null && gedcom.header.gedcomVersion != null
                && SupportedVersion.V5_5.equals(gedcom.header.gedcomVersion.versionNumber);
    }
}
TOP

Related Classes of org.gedcom4j.writer.GedcomWriter

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.