ca.nrc.cadc.caom2.xml.ObservationWriter.java Source code

Java tutorial

Introduction

Here is the source code for ca.nrc.cadc.caom2.xml.ObservationWriter.java

Source

/*
************************************************************************
*******************  CANADIAN ASTRONOMY DATA CENTRE  *******************
**************  CENTRE CANADIEN DE DONNES ASTRONOMIQUES  **************
*
*  (c) 2011.                            (c) 2011.
*  Government of Canada                 Gouvernement du Canada
*  National Research Council            Conseil national de recherches
*  Ottawa, Canada, K1A 0R6              Ottawa, Canada, K1A 0R6
*  All rights reserved                  Tous droits rservs
*
*  NRC disclaims any warranties,        Le CNRC dnie toute garantie
*  expressed, implied, or               nonce, implicite ou lgale,
*  statutory, of any kind with          de quelque nature que ce
*  respect to the software,             soit, concernant le logiciel,
*  including without limitation         y compris sans restriction
*  any warranty of merchantability      toute garantie de valeur
*  or fitness for a particular          marchande ou de pertinence
*  purpose. NRC shall not be            pour un usage particulier.
*  liable in any event for any          Le CNRC ne pourra en aucun cas
*  damages, whether direct or           tre tenu responsable de tout
*  indirect, special or general,        dommage, direct ou indirect,
*  consequential or incidental,         particulier ou gnral,
*  arising from the use of the          accessoire ou fortuit, rsultant
*  software.  Neither the name          de l'utilisation du logiciel. Ni
*  of the National Research             le nom du Conseil National de
*  Council of Canada nor the            Recherches du Canada ni les noms
*  names of its contributors may        de ses  participants ne peuvent
*  be used to endorse or promote        tre utiliss pour approuver ou
*  products derived from this           promouvoir les produits drivs
*  software without specific prior      de ce logiciel sans autorisation
*  written permission.                  pralable et particulire
*                                       par crit.
*
*  This file is part of the             Ce fichier fait partie du projet
*  OpenCADC project.                    OpenCADC.
*
*  OpenCADC is free software:           OpenCADC est un logiciel libre ;
*  you can redistribute it and/or       vous pouvez le redistribuer ou le
*  modify it under the terms of         modifier suivant les termes de
*  the GNU Affero General Public        la GNU Affero General Public
*  License as published by the          License? telle que publie
*  Free Software Foundation,            par la Free Software Foundation
*  either version 3 of the              : soit la version 3 de cette
*  License, or (at your option)         licence, soit ( votre gr)
*  any later version.                   toute version ultrieure.
*
*  OpenCADC is distributed in the       OpenCADC est distribu
*  hope that it will be useful,         dans lespoir quil vous
*  but WITHOUT ANY WARRANTY;            sera utile, mais SANS AUCUNE
*  without even the implied             GARANTIE : sans mme la garantie
*  warranty of MERCHANTABILITY          implicite de COMMERCIALISABILIT
*  or FITNESS FOR A PARTICULAR          ni dADQUATION  UN OBJECTIF
*  PURPOSE.  See the GNU Affero         PARTICULIER. Consultez la Licence
*  General Public License for           Gnrale Publique GNU Affero
*  more details.                        pour plus de dtails.
*
*  You should have received             Vous devriez avoir reu une
*  a copy of the GNU Affero             copie de la Licence Gnrale
*  General Public License along         Publique GNU Affero avec
*  with OpenCADC.  If not, see          OpenCADC ; si ce nest
*  <http://www.gnu.org/licenses/>.      pas le cas, consultez :
*                                       <http://www.gnu.org/licenses/>.
*
*  $Revision: 4 $
*
************************************************************************
*/

package ca.nrc.cadc.caom2.xml;

import ca.nrc.cadc.caom2.Algorithm;
import ca.nrc.cadc.caom2.Artifact;
import ca.nrc.cadc.caom2.CaomEntity;
import ca.nrc.cadc.caom2.Chunk;
import ca.nrc.cadc.caom2.CompositeObservation;
import ca.nrc.cadc.caom2.DataProductType;
import ca.nrc.cadc.caom2.DataQuality;
import ca.nrc.cadc.caom2.Energy;
import ca.nrc.cadc.caom2.EnergyTransition;
import ca.nrc.cadc.caom2.Environment;
import ca.nrc.cadc.caom2.Instrument;
import ca.nrc.cadc.caom2.Metrics;
import ca.nrc.cadc.caom2.Observation;
import ca.nrc.cadc.caom2.ObservationURI;
import ca.nrc.cadc.caom2.Part;
import ca.nrc.cadc.caom2.Plane;
import ca.nrc.cadc.caom2.PlaneURI;
import ca.nrc.cadc.caom2.Polarization;
import ca.nrc.cadc.caom2.PolarizationState;
import ca.nrc.cadc.caom2.Position;
import ca.nrc.cadc.caom2.Proposal;
import ca.nrc.cadc.caom2.Provenance;
import ca.nrc.cadc.caom2.Requirements;
import ca.nrc.cadc.caom2.Target;
import ca.nrc.cadc.caom2.TargetPosition;
import ca.nrc.cadc.caom2.Telescope;
import ca.nrc.cadc.caom2.Time;
import ca.nrc.cadc.caom2.types.Circle;
import ca.nrc.cadc.caom2.types.MultiPolygon;
import ca.nrc.cadc.caom2.types.Point;
import ca.nrc.cadc.caom2.types.Polygon;
import ca.nrc.cadc.caom2.types.SubInterval;
import ca.nrc.cadc.caom2.types.Vertex;
import ca.nrc.cadc.caom2.util.CaomUtil;
import ca.nrc.cadc.caom2.wcs.Axis;
import ca.nrc.cadc.caom2.wcs.Coord2D;
import ca.nrc.cadc.caom2.wcs.CoordAxis1D;
import ca.nrc.cadc.caom2.wcs.CoordAxis2D;
import ca.nrc.cadc.caom2.wcs.CoordBounds1D;
import ca.nrc.cadc.caom2.wcs.CoordBounds2D;
import ca.nrc.cadc.caom2.wcs.CoordCircle2D;
import ca.nrc.cadc.caom2.wcs.CoordError;
import ca.nrc.cadc.caom2.wcs.CoordFunction1D;
import ca.nrc.cadc.caom2.wcs.CoordFunction2D;
import ca.nrc.cadc.caom2.wcs.CoordPolygon2D;
import ca.nrc.cadc.caom2.wcs.CoordRange1D;
import ca.nrc.cadc.caom2.wcs.CoordRange2D;
import ca.nrc.cadc.caom2.wcs.Dimension2D;
import ca.nrc.cadc.caom2.wcs.ObservableAxis;
import ca.nrc.cadc.caom2.wcs.PolarizationWCS;
import ca.nrc.cadc.caom2.wcs.RefCoord;
import ca.nrc.cadc.caom2.wcs.Slice;
import ca.nrc.cadc.caom2.wcs.SpatialWCS;
import ca.nrc.cadc.caom2.wcs.SpectralWCS;
import ca.nrc.cadc.caom2.wcs.TemporalWCS;
import ca.nrc.cadc.caom2.wcs.ValueCoord2D;
import ca.nrc.cadc.date.DateUtil;
import ca.nrc.cadc.util.StringBuilderWriter;
import ca.nrc.cadc.util.StringUtil;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Serializable;
import java.io.UnsupportedEncodingException;
import java.io.Writer;
import java.net.URI;
import java.text.DateFormat;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.apache.log4j.Logger;
import org.jdom2.Document;
import org.jdom2.Element;
import org.jdom2.Namespace;
import org.jdom2.ProcessingInstruction;
import org.jdom2.output.Format;
import org.jdom2.output.XMLOutputter;

/**
 *
 * @author jburke
 */
public class ObservationWriter implements Serializable {
    private static final long serialVersionUID = 201604081100L;

    private static Logger log = Logger.getLogger(ObservationWriter.class);

    private String stylesheetURL;
    private boolean writeEmptyCollections;

    protected transient Namespace caom2Namespace;
    protected transient Namespace xsiNamespace;
    protected final int docVersion; // (int) (major.minor / 10) so CAOM-2.0 == 20

    /**
     * Default constructor. This uses a standard prefix <code>caom2</code>, does
     * not include empty elements for empty collections, and defaults to the
     * most recent target namespace.
     */
    public ObservationWriter() {
        this("caom2", null, false);
    }

    public ObservationWriter(String caom2prefix, boolean writeEmptyCollections) {
        this(caom2prefix, null, writeEmptyCollections);
    }

    /**
     * Constructor. This uses the specified CAOM namespace prefix (null is not
     * allowed). If writeEmptyCollections is true, empty elements will be
     * included for any collections that are empty; this is not necessary but is
     * valid in the schema so is useful for testing.
     *
     * @param caom2NamespacePrefix
     * @param namespace
     *            a valid CAOM-2.x target namespace
     * @param writeEmptyCollections
     */
    public ObservationWriter(String caom2NamespacePrefix, String namespace, boolean writeEmptyCollections) {
        this.writeEmptyCollections = writeEmptyCollections;
        if (!StringUtil.hasText(caom2NamespacePrefix)) {
            throw new IllegalArgumentException(
                    "null or 0-length namespace prefix is not allowed: " + caom2NamespacePrefix);
        }

        if (namespace == null) {
            namespace = XmlConstants.CAOM2_3_NAMESPACE; // default
            log.debug("default namespace: " + namespace);
        }

        if (XmlConstants.CAOM2_3_NAMESPACE.equals(namespace)) {
            this.caom2Namespace = Namespace.getNamespace(caom2NamespacePrefix, XmlConstants.CAOM2_3_NAMESPACE);
            docVersion = 23;
        } else if (XmlConstants.CAOM2_2_NAMESPACE.equals(namespace)) {
            this.caom2Namespace = Namespace.getNamespace(caom2NamespacePrefix, XmlConstants.CAOM2_2_NAMESPACE);
            docVersion = 22;
        } else if (XmlConstants.CAOM2_1_NAMESPACE.equals(namespace)) {
            this.caom2Namespace = Namespace.getNamespace(caom2NamespacePrefix, XmlConstants.CAOM2_1_NAMESPACE);
            docVersion = 21;
        } else if (XmlConstants.CAOM2_0_NAMESPACE.equals(namespace)) {
            this.caom2Namespace = Namespace.getNamespace(caom2NamespacePrefix, XmlConstants.CAOM2_0_NAMESPACE);
            docVersion = 20;
        } else {
            throw new IllegalArgumentException("invalid namespace: " + namespace);
        }

        this.xsiNamespace = Namespace.getNamespace("xsi", XmlConstants.XMLSCHEMA);

        log.debug("output version: " + docVersion + " " + caom2Namespace.getPrefix() + " -> "
                + caom2Namespace.getURI());
    }

    /**
     * 
     * @param stylesheetURL
     */
    public void setStylesheetURL(String stylesheetURL) {
        this.stylesheetURL = stylesheetURL;
    }

    /**
     * 
     * @return
     */
    public String getStylesheetURL() {
        return stylesheetURL;
    }

    /**
     * Set whether to write an empty Element when a java Collection is empty.
     * The default behavior is not to not write empty Collection elements.
     * 
     * @param b
     *            true to write elements for empty Collections, false to not
     *            write empty Collections.
     */
    public void setWriteEmptyCollections(boolean b) {
        writeEmptyCollections = b;
    }

    public boolean getWriteEmptyCollections() {
        return writeEmptyCollections;
    }

    /**
     * Write an Observation to an OutputStream using UTF-8 encoding.
     *
     * @param obs
     *            Observation to write.
     * @param out
     *            OutputStream to write to.
     * @throws IOException
     *             if the writer fails to write.
     */
    public void write(Observation obs, OutputStream out) throws IOException {
        OutputStreamWriter outWriter;
        try {
            outWriter = new OutputStreamWriter(out, "UTF-8");
        } catch (UnsupportedEncodingException e) {
            throw new RuntimeException("UTF-8 encoding not supported", e);
        }
        write(obs, outWriter);
    }

    /**
     * Write an Observation to a StringBuilder.
     * 
     * @param obs
     *            Observation to write.
     * @param builder
     *            StringBuilder to write to.
     * @throws IOException
     *             if the writer fails to write.
     */
    public void write(Observation obs, StringBuilder builder) throws IOException {
        write(obs, new StringBuilderWriter(builder));
    }

    /**
     * Write an Observation to a Writer.
     * 
     * @param obs
     *            Observation to write.
     * @param writer
     *            Writer to write to.
     * @throws IOException
     *             if the writer fails to write.
     */
    public void write(Observation obs, Writer writer) throws IOException {
        long start = System.currentTimeMillis();
        Element root = getRootElement(obs);
        write(root, writer);
        long end = System.currentTimeMillis();
        log.debug("Write elapsed time: " + (end - start) + "ms");
    }

    /**
     * Write the root Element to a writer.
     *
     * @param root
     *            Root Element to write.
     * @param writer
     *            Writer to write to.
     * @throws IOException
     *             if the writer fails to write.
     */
    @SuppressWarnings("unchecked")
    protected void write(Element root, Writer writer) throws IOException {
        XMLOutputter outputter = new XMLOutputter();
        outputter.setFormat(Format.getPrettyFormat());
        Document document = new Document(root);
        if (stylesheetURL != null) {
            Map<String, String> instructionMap = new HashMap<String, String>(2);
            instructionMap.put("type", "text/xsl");
            instructionMap.put("href", stylesheetURL);
            ProcessingInstruction pi = new ProcessingInstruction("xml-stylesheet", instructionMap);
            document.getContent().add(0, pi);
        }
        outputter.output(document, writer);
    }

    /**
     * Build the root Element of a Observation.
     *
     * @param obs
     *            Observation.
     * @return root an Observation Element.
     */
    protected Element getRootElement(Observation obs) {
        // Create the root element (observation).
        Element root = getObservationElement(obs);
        root.addNamespaceDeclaration(caom2Namespace);
        root.addNamespaceDeclaration(xsiNamespace);
        return root;
    }

    private void addEntityAttributes(CaomEntity ce, Element el, DateFormat df) {
        if (docVersion < 21) {
            el.setAttribute("id", CaomUtil.uuidToLong(ce.getID()).toString(), caom2Namespace);
        } else {
            el.setAttribute("id", ce.getID().toString(), caom2Namespace);
        }

        if (ce.getLastModified() != null) {
            el.setAttribute("lastModified", df.format(ce.getLastModified()), el.getNamespace());
        }

        if (docVersion >= 23 && ce.getMaxLastModified() != null) {
            el.setAttribute("maxLastModified", df.format(ce.getMaxLastModified()), el.getNamespace());
        }

        if (docVersion >= 23 && ce.getMetaChecksum() != null) {
            el.setAttribute("metaChecksum", ce.getMetaChecksum().toASCIIString(), el.getNamespace());
        }

        if (docVersion >= 23 && ce.getAccMetaChecksum() != null) {
            el.setAttribute("accMetaChecksum", ce.getAccMetaChecksum().toASCIIString(), el.getNamespace());
        }
    }

    /**
     * Builds a JDOM representation of an Observation.
     *
     * @param obs
     *            The Observation.
     * @return a JDOM Element representing the Observation.
     */
    protected Element getObservationElement(Observation obs) {
        // IVOA DateFormat.
        DateFormat dateFormat = DateUtil.getDateFormat(DateUtil.IVOA_DATE_FORMAT, DateUtil.UTC);

        Element element = getCaom2Element("Observation");
        String type = caom2Namespace.getPrefix() + ":" + obs.getClass().getSimpleName();
        element.setAttribute("type", type, xsiNamespace);

        addEntityAttributes(obs, element, dateFormat);

        addElement("collection", obs.getCollection(), element);
        addElement("observationID", obs.getObservationID(), element);

        // Observation elements.
        addDateElement("metaRelease", obs.metaRelease, element, dateFormat);
        addNumberElement("sequenceNumber", obs.sequenceNumber, element);
        addAlgorithmElement(obs.getAlgorithm(), element, dateFormat);
        addElement("type", obs.type, element);
        if (obs.intent != null) {
            addElement("intent", obs.intent.getValue(), element);
        }
        addProposalElement(obs.proposal, element, dateFormat);
        addTargetElement(obs.target, element, dateFormat);
        addTargetPositionElement(obs.targetPosition, element, dateFormat);
        addRequirements(obs.requirements, element, dateFormat);
        addTelescopeElement(obs.telescope, element, dateFormat);
        addInstrumentElement(obs.instrument, element, dateFormat);
        addEnvironmentElement(obs.environment, element, dateFormat);
        addPlanesElement(obs.getPlanes(), element, dateFormat);

        // Members must be the last element.
        if (obs instanceof CompositeObservation) {
            addMembersElement(((CompositeObservation) obs).getMembers(), element, dateFormat);
        }

        return element;
    }

    /**
     * Builds a JDOM representation of an Algorithm and adds it to the parent
     * element.
     *
     * @param algorithm
     *            The Algorithm to add to the parent.
     * @param parent
     *            The parent element for this child element.
     * @param dateFormat
     *            The IVOA DateFormat.
     */
    protected void addAlgorithmElement(Algorithm algorithm, Element parent, DateFormat dateFormat) {
        if (algorithm == null) {
            return;
        }

        Element element = getCaom2Element("algorithm");
        addElement("name", algorithm.getName(), element);
        parent.addContent(element);
    }

    /**
     * Builds a JDOM representation of an Proposal and adds it to the parent
     * element.
     *
     * @param proposal
     *            The Proposal to add to the parent.
     * @param parent
     *            The parent element for this child element.
     * @param dateFormat
     *            The IVOA DateFormat.
     */
    protected void addProposalElement(Proposal proposal, Element parent, DateFormat dateFormat) {
        if (proposal == null) {
            return;
        }

        Element element = getCaom2Element("proposal");
        addElement("id", proposal.getID(), element);
        addElement("pi", proposal.pi, element);
        addElement("project", proposal.project, element);
        addElement("title", proposal.title, element);
        if (docVersion < 23) {
            addStringListElement("keywords", proposal.getKeywords(), element);
        } else {
            addKeywordsElement(proposal.getKeywords(), element);
        }
        parent.addContent(element);
    }

    /**
     * Builds a JDOM representation of an Target and adds it to the parent
     * element.
     *
     * @param target
     *            The Target to add to the parent.
     * @param parent
     *            The parent element for this child element.
     * @param dateFormat
     *            The IVOA DateFormat.
     */
    protected void addTargetElement(Target target, Element parent, DateFormat dateFormat) {
        if (target == null) {
            return;
        }

        Element element = getCaom2Element("target");
        addElement("name", target.getName(), element);
        if (target.type != null) {
            addElement("type", target.type.getValue(), element);
        }
        addBooleanElement("standard", target.standard, element);
        addNumberElement("redshift", target.redshift, element);
        addBooleanElement("moving", target.moving, element);
        if (docVersion < 23) {
            addStringListElement("keywords", target.getKeywords(), element);
        } else {
            addKeywordsElement(target.getKeywords(), element);
        }
        parent.addContent(element);
    }

    /**
     * Builds a JDOM representation of an TargetPosition and adds it to the
     * parent element.
     *
     * @param targetPosition
     *            The Target to add to the parent.
     * @param parent
     *            The parent element for this child element.
     * @param dateFormat
     *            The IVOA DateFormat.
     */
    protected void addTargetPositionElement(TargetPosition targetPosition, Element parent, DateFormat dateFormat) {
        if (targetPosition == null) {
            return;
        }

        Element element = getCaom2Element("targetPosition");
        Element coordsys = getCaom2Element("coordsys");
        coordsys.addContent(targetPosition.getCoordsys());
        element.addContent(coordsys);
        if (targetPosition.equinox != null) {
            Element equinox = getCaom2Element("equinox");
            equinox.addContent(targetPosition.equinox.toString());
            element.addContent(equinox);
        }
        Element coords = getCaom2Element("coordinates");
        addNumberElement("cval1", targetPosition.getCoordinates().cval1, coords);
        addNumberElement("cval2", targetPosition.getCoordinates().cval2, coords);
        element.addContent(coords);
        parent.addContent(element);
    }

    /**
     * Add a JDOM representation of Requirements.
     * 
     * @param req
     * @param parent
     * @param dateFormat
     */
    protected void addRequirements(Requirements req, Element parent, DateFormat dateFormat) {
        if (docVersion < 21) {
            return; // Requirements added in CAOM-2.1
        }
        if (req == null) {
            return;
        }
        Element element = getCaom2Element("requirements");
        Element flag = getCaom2Element("flag");
        flag.addContent(req.getFlag().getValue());
        element.addContent(flag);
        parent.addContent(element);
    }

    /**
     * Builds a JDOM representation of an Telescope and adds it to the parent
     * element.
     *
     * @param telescope
     *            The Telescope to add to the parent.
     * @param parent
     *            The parent element for this child element.
     * @param dateFormat
     *            The IVOA DateFormat.
     */
    protected void addTelescopeElement(Telescope telescope, Element parent, DateFormat dateFormat) {
        if (telescope == null) {
            return;
        }

        Element element = getCaom2Element("telescope");
        addElement("name", telescope.getName(), element);
        addNumberElement("geoLocationX", telescope.geoLocationX, element);
        addNumberElement("geoLocationY", telescope.geoLocationY, element);
        addNumberElement("geoLocationZ", telescope.geoLocationZ, element);
        if (docVersion < 23) {
            addStringListElement("keywords", telescope.getKeywords(), element);
        } else {
            addKeywordsElement(telescope.getKeywords(), element);
        }
        parent.addContent(element);
    }

    /**
     * Builds a JDOM representation of an Instrument and adds it to the parent
     * element.
     *
     * @param instrument
     *            The Instrument to add to the parent.
     * @param parent
     *            The parent element for this child element.
     * @param dateFormat
     *            The IVOA DateFormat.
     */
    protected void addInstrumentElement(Instrument instrument, Element parent, DateFormat dateFormat) {
        if (instrument == null) {
            return;
        }

        Element element = getCaom2Element("instrument");
        addElement("name", instrument.getName(), element);
        if (docVersion < 23) {
            addStringListElement("keywords", instrument.getKeywords(), element);
        } else {
            addKeywordsElement(instrument.getKeywords(), element);
        }
        parent.addContent(element);
    }

    /**
     * Builds a JDOM representation of an Environment and adds it to the parent
     * element.
     *
     * @param environment
     *            The Environment to add to the parent.
     * @param parent
     *            The parent element for this child element.
     * @param dateFormat
     *            The IVOA DateFormat.
     */
    protected void addEnvironmentElement(Environment environment, Element parent, DateFormat dateFormat) {
        if (environment == null) {
            return;
        }

        Element element = getCaom2Element("environment");
        addNumberElement("seeing", environment.seeing, element);
        addNumberElement("humidity", environment.humidity, element);
        addNumberElement("elevation", environment.elevation, element);
        addNumberElement("tau", environment.tau, element);
        addNumberElement("wavelengthTau", environment.wavelengthTau, element);
        addNumberElement("ambientTemp", environment.ambientTemp, element);
        addBooleanElement("photometric", environment.photometric, element);
        parent.addContent(element);
    }

    /**
     * Builds a JDOM representation of a Set of ObservationURI and adds it to
     * the parent element.
     *
     * @param members
     *            The Set of ObservationURI to add to the parent.
     * @param parent
     *            The parent element for this child element.
     * @param dateFormat
     *            The IVOA DateFormat.
     */
    protected void addMembersElement(Set<ObservationURI> members, Element parent, DateFormat dateFormat) {
        if (members == null || (members.isEmpty() && !writeEmptyCollections)) {
            return;
        }

        Element element = getCaom2Element("members");
        for (ObservationURI member : members) {
            addURIElement("observationURI", member.getURI(), element);
        }
        parent.addContent(element);
    }

    /**
     * Builds a JDOM representation of a Set of Plane's and adds it to the
     * parent element.
     *
     * @param planes
     *            The Set of Plane's to add to the parent.
     * @param parent
     *            The parent element for this child element.
     * @param dateFormat
     *            The IVOA DateFormat.
     */
    protected void addPlanesElement(Set<Plane> planes, Element parent, DateFormat dateFormat) {
        if (planes == null || (planes.isEmpty() && !writeEmptyCollections)) {
            return;
        }

        Element element = getCaom2Element("planes");
        for (Plane plane : planes) {
            Element planeElement = getCaom2Element("plane");
            addEntityAttributes(plane, planeElement, dateFormat);
            addElement("productID", plane.getProductID(), planeElement);

            if (docVersion >= 23 && plane.creatorID != null) {
                addURIElement("creatorID", plane.creatorID, planeElement);
            }

            addDateElement("metaRelease", plane.metaRelease, planeElement, dateFormat);
            addDateElement("dataRelease", plane.dataRelease, planeElement, dateFormat);
            if (plane.dataProductType != null) {
                if (docVersion < 23 && DataProductType.CATALOG.equals(plane.dataProductType)) {
                    addElement("dataProductType", plane.dataProductType.getTerm(), planeElement);
                } else {
                    addElement("dataProductType", plane.dataProductType.getValue(), planeElement);
                }
            }
            if (plane.calibrationLevel != null) {
                addElement("calibrationLevel", String.valueOf(plane.calibrationLevel.getValue()), planeElement);
            }
            addProvenanceElement(plane.provenance, planeElement, dateFormat);
            addMetricsElement(plane.metrics, planeElement, dateFormat);
            addQuaility(plane.quality, planeElement, dateFormat);

            addPositionElement(plane.position, planeElement);
            addEnergyElement(plane.energy, planeElement);
            addTimeElement(plane.time, planeElement);
            addPolarizationElement(plane.polarization, planeElement);

            addArtifactsElement(plane.getArtifacts(), planeElement, dateFormat);
            element.addContent(planeElement);
        }
        parent.addContent(element);
    }

    protected void addPositionElement(Position comp, Element parent) {
        if (docVersion < 22) {
            return;
        }
        if (comp == null) {
            return;
        }

        Element e = getCaom2Element("position");
        if (comp.bounds != null) {
            if (comp.bounds instanceof Circle) {
                Circle circ = (Circle) comp.bounds;
                Element pe = getCaom2Element("bounds");
                String xsiType = caom2Namespace.getPrefix() + ":" + Circle.class.getSimpleName();
                pe.setAttribute("type", xsiType, xsiNamespace);

                Element ce = getCaom2Element("center");
                addNumberElement("cval1", circ.getCenter().cval1, ce);
                addNumberElement("cval2", circ.getCenter().cval2, ce);
                pe.addContent(ce);
                addNumberElement("radius", circ.getRadius(), pe);
                e.addContent(pe);
            } else if (comp.bounds instanceof Polygon) {
                if (docVersion < 23) {
                    throw new UnsupportedOperationException("cannot downgrade polygon doc version " + docVersion);
                }
                Polygon poly = (Polygon) comp.bounds;
                Element pe = getCaom2Element("bounds");
                String xsiType = caom2Namespace.getPrefix() + ":" + Polygon.class.getSimpleName();
                pe.setAttribute("type", xsiType, xsiNamespace);

                Element pes = getCaom2Element("points");

                for (Point p : poly.getPoints()) {
                    Element ppe = getCaom2Element("point");
                    addNumberElement("cval1", p.cval1, ppe);
                    addNumberElement("cval2", p.cval2, ppe);
                    pes.addContent(ppe);
                }
                pe.addContent(pes);

                Element se = getCaom2Element("samples");
                Element ves = getCaom2Element("vertices");
                MultiPolygon mp = poly.getSamples();
                for (Vertex v : mp.getVertices()) {
                    Element ve = getCaom2Element("vertex");
                    addNumberElement("cval1", v.cval1, ve);
                    addNumberElement("cval2", v.cval2, ve);
                    addNumberElement("type", v.getType().getValue(), ve);
                    ves.addContent(ve);
                }
                se.addContent(ves);
                pe.addContent(se);
                e.addContent(pe);
            } else {
                throw new UnsupportedOperationException(comp.bounds.getClass().getName() + " -> XML");
            }
        }
        if (comp.dimension != null) {
            Element ce = getCaom2Element("dimension");
            // String xsiType = caom2Namespace.getPrefix() + ":" +
            // Dimension2D.class.getSimpleName();
            // ce.setAttribute("type", xsiType, xsiNamespace);
            addNumberElement("naxis1", comp.dimension.naxis1, ce);
            addNumberElement("naxis2", comp.dimension.naxis2, ce);
            e.addContent(ce);
        }
        if (comp.resolution != null) {
            addNumberElement("resolution", comp.resolution, e);
        }
        if (comp.sampleSize != null) {
            addNumberElement("sampleSize", comp.sampleSize, e);
        }
        if (comp.timeDependent != null) {
            addBooleanElement("timeDependent", comp.timeDependent, e);
        }
        parent.addContent(e);
    }

    protected Element getSampleElement(SubInterval si) {
        Element s = getCaom2Element("sample");
        addNumberElement("lower", si.getLower(), s);
        addNumberElement("upper", si.getUpper(), s);
        return s;
    }

    protected void addEnergyElement(Energy comp, Element parent) {
        if (docVersion < 22) {
            return;
        }
        if (comp == null) {
            return;
        }

        Element e = getCaom2Element("energy");
        if (comp.bounds != null) {
            Element pe = getCaom2Element("bounds");
            // String xsiType = caom2Namespace.getPrefix() + ":" +
            // Interval.class.getSimpleName();
            // pe.setAttribute("type", xsiType, xsiNamespace);
            addNumberElement("lower", comp.bounds.getLower(), pe);
            addNumberElement("upper", comp.bounds.getUpper(), pe);
            e.addContent(pe);
            if (!comp.bounds.getSamples().isEmpty()) {
                Element ses = getCaom2Element("samples");
                for (SubInterval si : comp.bounds.getSamples()) {
                    Element se = getSampleElement(si);
                    ses.addContent(se);
                }
                pe.addContent(ses);
            }
        }
        if (comp.dimension != null) {
            Element ce = getCaom2Element("dimension");
            // String xsiType = caom2Namespace.getPrefix() + ":" +
            // Long.class.getSimpleName();
            // ce.setAttribute("type", xsiType, xsiNamespace);
            ce.addContent(Long.toString(comp.dimension));
            e.addContent(ce);
        }
        if (comp.resolvingPower != null) {
            addNumberElement("resolvingPower", comp.resolvingPower, e);
        }
        if (comp.sampleSize != null) {
            addNumberElement("sampleSize", comp.sampleSize, e);
        }
        if (comp.bandpassName != null) {
            addElement("bandpassName", comp.bandpassName, e);
        }
        if (comp.emBand != null) {
            addElement("emBand", comp.emBand.getValue(), e);
        }
        if (comp.restwav != null) {
            addNumberElement("restwav", comp.restwav, e);
        }
        if (comp.transition != null) {
            Element ce = getCaom2Element("transition");
            addElement("species", comp.transition.getSpecies(), ce);
            addElement("transition", comp.transition.getTransition(), ce);
            e.addContent(ce);
        }

        parent.addContent(e);
    }

    protected void addTimeElement(Time comp, Element parent) {
        if (docVersion < 22) {
            return;
        }
        if (comp == null) {
            return;
        }

        Element e = getCaom2Element("time");
        if (comp.bounds != null) {
            Element pe = getCaom2Element("bounds");
            // String xsiType = caom2Namespace.getPrefix() + ":" +
            // Interval.class.getSimpleName();
            // pe.setAttribute("type", xsiType, xsiNamespace);
            addNumberElement("lower", comp.bounds.getLower(), pe);
            addNumberElement("upper", comp.bounds.getUpper(), pe);
            e.addContent(pe);
            if (!comp.bounds.getSamples().isEmpty()) {
                Element ses = getCaom2Element("samples");
                for (SubInterval si : comp.bounds.getSamples()) {
                    Element se = getSampleElement(si);
                    ses.addContent(se);
                }
                pe.addContent(ses);
            }
        }
        if (comp.dimension != null) {
            Element ce = getCaom2Element("dimension");
            // String xsiType = caom2Namespace.getPrefix() + ":" +
            // Long.class.getSimpleName();
            // ce.setAttribute("type", xsiType, xsiNamespace);
            ce.addContent(Long.toString(comp.dimension));
            e.addContent(ce);
        }
        if (comp.resolution != null) {
            addNumberElement("resolution", comp.resolution, e);
        }
        if (comp.sampleSize != null) {
            addNumberElement("sampleSize", comp.sampleSize, e);
        }
        if (comp.exposure != null) {
            addNumberElement("exposure", comp.exposure, e);
        }

        parent.addContent(e);
    }

    protected void addPolarizationElement(Polarization comp, Element parent) {
        if (docVersion < 22) {
            return;
        }
        if (comp == null) {
            return;
        }

        Element e = getCaom2Element("polarization");
        if (comp.states != null) {
            Element pe = getCaom2Element("states");
            for (PolarizationState s : comp.states) {
                addElement("state", s.stringValue(), pe);
            }
            e.addContent(pe);
        }
        if (comp.dimension != null) {
            Element ce = getCaom2Element("dimension");
            // String xsiType = caom2Namespace.getPrefix() + ":" +
            // Integer.class.getSimpleName();
            // ce.setAttribute("type", xsiType, xsiNamespace);
            ce.addContent(Long.toString(comp.dimension));
            e.addContent(ce);
        }

        parent.addContent(e);
    }

    /**
     * Builds a JDOM representation of an Telescope and adds it to the parent
     * element.
     *
     * @param provenance
     *            The Provenance to add to the parent.
     * @param parent
     *            The parent element for this child element.
     * @param dateFormat
     *            The IVOA DateFormat.
     */
    protected void addProvenanceElement(Provenance provenance, Element parent, DateFormat dateFormat) {
        if (provenance == null) {
            return;
        }

        Element element = getCaom2Element("provenance");
        addElement("name", provenance.getName(), element);
        addElement("version", provenance.version, element);
        addElement("project", provenance.project, element);
        addElement("producer", provenance.producer, element);
        addElement("runID", provenance.runID, element);
        addURIElement("reference", provenance.reference, element);
        addDateElement("lastExecuted", provenance.lastExecuted, element, dateFormat);
        if (docVersion < 23) {
            addStringListElement("keywords", provenance.getKeywords(), element);
        } else {
            addKeywordsElement(provenance.getKeywords(), element);
        }
        addInputsElement(provenance.getInputs(), element, dateFormat);
        parent.addContent(element);
    }

    protected void addMetricsElement(Metrics metrics, Element parent, DateFormat dateFormat) {
        if (metrics == null) {
            return;
        }

        Element element = getCaom2Element("metrics");
        addNumberElement("sourceNumberDensity", metrics.sourceNumberDensity, element);
        addNumberElement("background", metrics.background, element);
        addNumberElement("backgroundStddev", metrics.backgroundStddev, element);
        addNumberElement("fluxDensityLimit", metrics.fluxDensityLimit, element);
        addNumberElement("magLimit", metrics.magLimit, element);
        parent.addContent(element);
    }

    /**
     * Add a JDOM representation of DaatQuality.
     * 
     * @param dq
     * @param parent
     * @param dateFormat
     */
    protected void addQuaility(DataQuality dq, Element parent, DateFormat dateFormat) {
        if (docVersion < 21) {
            return; // DataQuality added in CAOM-2.1
        }
        if (dq == null) {
            return;
        }
        Element element = getCaom2Element("quality");
        Element flag = getCaom2Element("flag");
        flag.addContent(dq.getFlag().getValue());
        element.addContent(flag);
        parent.addContent(element);
    }

    protected void addTransitionElement(EnergyTransition transition, Element parent, DateFormat dateFormat) {
        if (transition == null) {
            return;
        }

        Element element = getCaom2Element("transition");
        addElement("species", transition.getSpecies(), element);
        addElement("transition", transition.getTransition(), element);
        parent.addContent(element);
    }

    /**
     * Builds a JDOM representation of a Set of PlaneURI and adds it to the
     * parent element.
     *
     * @param inputs
     *            The Set of PlaneURI to add to the parent.
     * @param parent
     *            The parent element for this child element.
     * @param dateFormat
     *            The IVOA DateFormat.
     */
    protected void addInputsElement(Set<PlaneURI> inputs, Element parent, DateFormat dateFormat) {
        if (inputs == null || (inputs.isEmpty() && !writeEmptyCollections)) {
            return;
        }

        Element element = getCaom2Element("inputs");
        for (PlaneURI input : inputs) {
            addURIElement("planeURI", input.getURI(), element);
        }
        parent.addContent(element);
    }

    /**
     * Builds a JDOM representation of a Set of Artifact's and adds it to the
     * parent element.
     *
     * @param artifacts
     *            The Set of Artifact's to add to the parent.
     * @param parent
     *            The parent element for this child element.
     * @param dateFormat
     *            IVOA DateFormat.
     */
    protected void addArtifactsElement(Set<Artifact> artifacts, Element parent, DateFormat dateFormat) {
        if (artifacts == null || (artifacts.isEmpty() && !writeEmptyCollections)) {
            return;
        }

        Element element = getCaom2Element("artifacts");
        for (Artifact artifact : artifacts) {
            Element artifactElement = getCaom2Element("artifact");
            addEntityAttributes(artifact, artifactElement, dateFormat);
            addURIElement("uri", artifact.getURI(), artifactElement);

            if (docVersion >= 22) {
                addElement("productType", artifact.getProductType().getValue(), artifactElement);
                addElement("releaseType", artifact.getReleaseType().getValue(), artifactElement);
            }

            addElement("contentType", artifact.contentType, artifactElement);
            addNumberElement("contentLength", artifact.contentLength, artifactElement);

            if (docVersion < 22) {
                addElement("productType", artifact.getProductType().getValue(), artifactElement);
            }

            if (docVersion > 22) {
                addURIElement("contentChecksum", artifact.contentChecksum, artifactElement);
            }

            addPartsElement(artifact.getParts(), artifactElement, dateFormat);
            element.addContent(artifactElement);
        }
        parent.addContent(element);
    }

    /**
     * Builds a JDOM representation of a Set of Part's and adds it to the parent
     * element.
     *
     * @param parts
     *            The Set of Part's to add to the parent.
     * @param parent
     *            The parent element for this child element.
     * @param dateFormat
     *            The IVOA DateFormat.
     */
    protected void addPartsElement(Set<Part> parts, Element parent, DateFormat dateFormat) {
        if (parts == null || (parts.isEmpty() && !writeEmptyCollections)) {
            return;
        }

        Element element = getCaom2Element("parts");
        for (Part part : parts) {
            Element partElement = getCaom2Element("part");
            addEntityAttributes(part, partElement, dateFormat);
            addElement("name", part.getName(), partElement);
            if (part.productType != null) {
                addElement("productType", part.productType.getValue(), partElement);
            }
            addChunksElement(part.getChunks(), partElement, dateFormat);
            element.addContent(partElement);
        }
        parent.addContent(element);
    }

    /**
     * Builds a JDOM representation of a Set of Chunk's and adds it to the
     * parent element.
     *
     * @param chunks
     *            The Set of Chunk's to add to the parent.
     * @param parent
     *            The parent element for this child element.
     * @param dateFormat
     *            The IVOA DateFormat.
     */
    protected void addChunksElement(Set<Chunk> chunks, Element parent, DateFormat dateFormat) {
        if (chunks == null || (chunks.isEmpty() && !writeEmptyCollections)) {
            return;
        }

        Element element = getCaom2Element("chunks");
        for (Chunk chunk : chunks) {
            Element chunkElement = getCaom2Element("chunk");
            addEntityAttributes(chunk, chunkElement, dateFormat);
            if (chunk.productType != null) {
                addElement("productType", chunk.productType.getValue(), chunkElement);
            }
            addNumberElement("naxis", chunk.naxis, chunkElement);
            addNumberElement("observableAxis", chunk.observableAxis, chunkElement);
            addNumberElement("positionAxis1", chunk.positionAxis1, chunkElement);
            addNumberElement("positionAxis2", chunk.positionAxis2, chunkElement);
            addNumberElement("energyAxis", chunk.energyAxis, chunkElement);
            addNumberElement("timeAxis", chunk.timeAxis, chunkElement);
            addNumberElement("polarizationAxis", chunk.polarizationAxis, chunkElement);

            addObservableAxisElement(chunk.observable, chunkElement, dateFormat);
            addSpatialWCSElement(chunk.position, chunkElement, dateFormat);
            addSpectralWCSElement(chunk.energy, chunkElement, dateFormat);
            addTemporalWCSElement(chunk.time, chunkElement, dateFormat);
            addPolarizationWCSElement(chunk.polarization, chunkElement, dateFormat);

            element.addContent(chunkElement);
        }
        parent.addContent(element);
    }

    /*
     * // alt version for one-chunk-per-part that was reverted from caom-2.2
     * protected void addChunksElement(Chunk chunk, Element parent, DateFormat
     * dateFormat) { if (chunk == null) return;
     * 
     * Element chunkParent = parent; if (docVersion < 22) { Element chunks =
     * getCaom2Element("chunks"); parent.addContent(chunks); chunkParent =
     * chunks; }
     * 
     * Element chunkElement = getCaom2Element("chunk");
     * addEntityAttributes(chunk, chunkElement, dateFormat);
     * addNumberElement("naxis", chunk.naxis, chunkElement);
     * addNumberElement("observableAxis", chunk.observableAxis, chunkElement);
     * addNumberElement("positionAxis1", chunk.positionAxis1, chunkElement);
     * addNumberElement("positionAxis2", chunk.positionAxis2, chunkElement);
     * addNumberElement("energyAxis", chunk.energyAxis, chunkElement);
     * addNumberElement("timeAxis", chunk.timeAxis, chunkElement);
     * addNumberElement("polarizationAxis", chunk.polarizationAxis,
     * chunkElement);
     * 
     * addObservableAxisElement(chunk.observable, chunkElement, dateFormat);
     * addSpatialWCSElement(chunk.position, chunkElement, dateFormat);
     * addSpectralWCSElement(chunk.energy, chunkElement, dateFormat);
     * addTemporalWCSElement(chunk.time, chunkElement, dateFormat);
     * addPolarizationWCSElement(chunk.polarization, chunkElement, dateFormat);
     * 
     * chunkParent.addContent(chunkElement); }
     */

    /**
     * Builds a JDOM representation of an ObservableAxis and adds it to the
     * parent element.
     *
     * @param observable
     *            The ObservableAxis to add to the parent.
     * @param parent
     *            The parent element for this child element.
     * @param dateFormat
     *            The IVOA DateFormat.
     */
    protected void addObservableAxisElement(ObservableAxis observable, Element parent, DateFormat dateFormat) {
        if (observable == null) {
            return;
        }

        Element element = getCaom2Element("observable");
        addSliceElement("dependent", observable.getDependent(), element);
        addSliceElement("independent", observable.independent, element);

        parent.addContent(element);
    }

    /**
     * Builds a JDOM representation of a SpatialWCS and adds it to the parent
     * element.
     *
     * @param position
     *            The SpatialWCS to add to the parent.
     * @param parent
     *            The parent element for this child element.
     * @param dateFormat
     *            The IVOA DateFormat.
     */
    protected void addSpatialWCSElement(SpatialWCS position, Element parent, DateFormat dateFormat) {
        if (position == null) {
            return;
        }

        Element element = getCaom2Element("position");
        addCoordAxis2DElement("axis", position.getAxis(), element);
        addElement("coordsys", position.coordsys, element);
        addNumberElement("equinox", position.equinox, element);
        addNumberElement("resolution", position.resolution, element);
        parent.addContent(element);
    }

    /**
     * Builds a JDOM representation of a SpectralWCS and adds it to the parent
     * element.
     *
     * @param energy
     *            The SpectralWCS to add to the parent.
     * @param parent
     *            The parent element for this child element.
     * @param dateFormat
     *            The IVOA DateFormat.
     */
    protected void addSpectralWCSElement(SpectralWCS energy, Element parent, DateFormat dateFormat) {
        if (energy == null) {
            return;
        }

        Element element = getCaom2Element("energy");
        addCoordAxis1DElement("axis", energy.getAxis(), element);
        addElement("specsys", energy.getSpecsys(), element);
        addElement("ssysobs", energy.ssysobs, element);
        addElement("ssyssrc", energy.ssyssrc, element);
        addNumberElement("restfrq", energy.restfrq, element);
        addNumberElement("restwav", energy.restwav, element);
        addNumberElement("velosys", energy.velosys, element);
        addNumberElement("zsource", energy.zsource, element);
        addNumberElement("velang", energy.velang, element);
        addElement("bandpassName", energy.bandpassName, element);
        addNumberElement("resolvingPower", energy.resolvingPower, element);
        addTransitionElement(energy.transition, element, dateFormat);
        parent.addContent(element);
    }

    /**
     * Builds a JDOM representation of a TemporalWCS and adds it to the parent
     * element.
     *
     * @param time
     *            The TemporalWCS to add to the parent.
     * @param parent
     *            The parent element for this child element.
     * @param dateFormat
     *            The IVOA DateFormat.
     */
    protected void addTemporalWCSElement(TemporalWCS time, Element parent, DateFormat dateFormat) {
        if (time == null) {
            return;
        }

        Element element = getCaom2Element("time");
        addCoordAxis1DElement("axis", time.getAxis(), element);
        addElement("timesys", time.timesys, element);
        addElement("trefpos", time.trefpos, element);
        addNumberElement("mjdref", time.mjdref, element);
        addNumberElement("exposure", time.exposure, element);
        addNumberElement("resolution", time.resolution, element);
        parent.addContent(element);
    }

    /**
     * Builds a JDOM representation of a PolarizationWCS and adds it to the
     * parent element.
     *
     * @param polarization
     *            The PolarizationWCS to add to the parent.
     * @param parent
     *            The parent element for this child element.
     * @param dateFormat
     *            The IVOA DateFormat.
     */
    protected void addPolarizationWCSElement(PolarizationWCS polarization, Element parent, DateFormat dateFormat) {
        if (polarization == null) {
            return;
        }

        Element element = getCaom2Element("polarization");
        addCoordAxis1DElement("axis", polarization.getAxis(), element);
        parent.addContent(element);
    }

    /*
     * WCS Types
     */

    /**
     * Builds a JDOM representation of a Axis and adds it to the parent element.
     * 
     * @param name
     *            The name of the element.
     * @param axis
     *            The Axis to add to the parent.
     * @param parent
     *            The parent element for this child element.
     */
    protected void addAxisElement(String name, Axis axis, Element parent) {
        if (axis == null) {
            return;
        }

        Element element = getCaom2Element(name);
        addElement("ctype", axis.getCtype(), element);
        addElement("cunit", axis.getCunit(), element);
        parent.addContent(element);
    }

    /**
     * Builds a JDOM representation of a Coord2D and adds it to the parent
     * element.
     * 
     * @param name
     *            The name of the element.
     * @param coord
     *            The Coord2D to add to the parent.
     * @param parent
     *            The parent element for this child element.
     */
    protected void addCoord2DElement(String name, Coord2D coord, Element parent) {
        if (coord == null) {
            return;
        }

        Element element = getCaom2Element(name);
        addRefCoordElement("coord1", coord.getCoord1(), element);
        addRefCoordElement("coord2", coord.getCoord2(), element);
        parent.addContent(element);
    }

    /**
     * Builds a JDOM representation of a ValueCoord2D and adds it to the parent
     * element.
     * 
     * @param name
     *            The name of the element.
     * @param coord
     *            The ValueCoord2D to add to the parent.
     * @param parent
     *            The parent element for this child element.
     */
    protected void addValueCoord2DElement(String name, ValueCoord2D coord, Element parent) {
        if (coord == null) {
            return;
        }

        Element element = getCaom2Element(name);
        addNumberElement("coord1", coord.coord1, element);
        addNumberElement("coord2", coord.coord2, element);
        parent.addContent(element);
    }

    /**
     * Builds a JDOM representation of a CoordAxis1D and adds it to the parent
     * element.
     * 
     * @param name
     *            The name of the element.
     * @param axis
     *            The CoordAxis1D to add to the parent.
     * @param parent
     *            The parent element for this child element.
     */
    protected void addCoordAxis1DElement(String name, CoordAxis1D axis, Element parent) {
        if (axis == null) {
            return;
        }

        Element element = getCaom2Element(name);
        addAxisElement("axis", axis.getAxis(), element);
        addCoordErrorElement("error", axis.error, element);
        addCoordRange1DElement("range", axis.range, element);
        addCoordBounds1DElement("bounds", axis.bounds, element);
        addCoordFunction1DElement("function", axis.function, element);
        parent.addContent(element);
    }

    /**
     * Builds a JDOM representation of a CoordAxis2D and adds it to the parent
     * element.
     * 
     * @param name
     *            The name of the element.
     * @param axis
     *            The CoordAxis2D to add to the parent.
     * @param parent
     *            The parent element for this child element.
     */
    protected void addCoordAxis2DElement(String name, CoordAxis2D axis, Element parent) {
        if (axis == null) {
            return;
        }

        Element element = getCaom2Element(name);
        addAxisElement("axis1", axis.getAxis1(), element);
        addAxisElement("axis2", axis.getAxis2(), element);
        addCoordErrorElement("error1", axis.error1, element);
        addCoordErrorElement("error2", axis.error2, element);
        addCoordRange2DElement("range", axis.range, element);
        addCoordBounds2DElement("bounds", axis.bounds, element);
        addCoordFunction2DElement("function", axis.function, element);
        parent.addContent(element);
    }

    /**
     * Builds a JDOM representation of a CoordBounds1D and adds it to the parent
     * element.
     * 
     * @param name
     *            The name of the element.
     * @param bounds
     *            The CoordBounds1D to add to the parent.
     * @param parent
     *            The parent element for this child element.
     */
    protected void addCoordBounds1DElement(String name, CoordBounds1D bounds, Element parent) {
        if (bounds == null) {
            return;
        }

        Element element = getCaom2Element(name);
        addCoordRange1DListElement("samples", bounds.getSamples(), element);
        parent.addContent(element);
    }

    /**
     * Builds a JDOM representation of a CoordBounds2D and adds it to the parent
     * element.
     * 
     * @param name
     *            The name of the element.
     * @param bounds
     *            The CoordBounds2D to add to the parent.
     * @param parent
     *            The parent element for this child element.
     */
    protected void addCoordBounds2DElement(String name, CoordBounds2D bounds, Element parent) {
        if (bounds == null) {
            return;
        }

        Element element = getCaom2Element(name);
        if (bounds instanceof CoordCircle2D) {
            addCoordCircle2DElement("circle", (CoordCircle2D) bounds, element);
        } else if (bounds instanceof CoordPolygon2D) {
            addCoordPolygon2DElement("polygon", (CoordPolygon2D) bounds, element);
        } else {
            throw new IllegalStateException(
                    "BUG: unsupported CoordBounds2D type " + bounds.getClass().getSimpleName());
        }
        parent.addContent(element);
    }

    /**
     * Builds a JDOM representation of a CoordCircle2D and adds it to the parent
     * element.
     * 
     * @param name
     *            The name of the element.
     * @param circle
     *            The CoordCircle2D to add to the parent.
     * @param parent
     *            The parent element for this child element.
     */
    protected void addCoordCircle2DElement(String name, CoordCircle2D circle, Element parent) {
        if (circle == null) {
            return;
        }

        Element element = getCaom2Element(name);
        addValueCoord2DElement("center", circle.getCenter(), element);
        addNumberElement("radius", circle.getRadius(), element);
        parent.addContent(element);
    }

    /**
     * Builds a JDOM representation of a CoordError and adds it to the parent
     * element.
     * 
     * @param name
     *            The name of the element.
     * @param error
     *            The CoordError to add to the parent.
     * @param parent
     *            The parent element for this child element.
     */
    protected void addCoordErrorElement(String name, CoordError error, Element parent) {
        if (error == null) {
            return;
        }

        Element element = getCaom2Element(name);
        addNumberElement("syser", error.syser, element);
        addNumberElement("rnder", error.rnder, element);
        parent.addContent(element);
    }

    /**
     * Builds a JDOM representation of a CoordFunction1D and adds it to the
     * parent element.
     * 
     * @param name
     *            The name of the element.
     * @param function
     *            The CoordFunction1D to add to the parent.
     * @param parent
     *            The parent element for this child element.
     */
    protected void addCoordFunction1DElement(String name, CoordFunction1D function, Element parent) {
        if (function == null) {
            return;
        }

        Element element = getCaom2Element(name);
        addNumberElement("naxis", function.getNaxis(), element);
        addNumberElement("delta", function.getDelta(), element);
        addRefCoordElement("refCoord", function.getRefCoord(), element);
        parent.addContent(element);
    }

    /**
     * Builds a JDOM representation of a CoordFunction2D and adds it to the
     * parent element.
     * 
     * @param name
     *            The name of the element.
     * @param function
     *            The CoordFunction2D to add to the parent.
     * @param parent
     *            The parent element for this child element.
     */
    protected void addCoordFunction2DElement(String name, CoordFunction2D function, Element parent) {
        if (function == null) {
            return;
        }

        Element element = getCaom2Element(name);
        addDimension2DElement("dimension", function.getDimension(), element);
        addCoord2DElement("refCoord", function.getRefCoord(), element);
        addNumberElement("cd11", function.getCd11(), element);
        addNumberElement("cd12", function.getCd12(), element);
        addNumberElement("cd21", function.getCd21(), element);
        addNumberElement("cd22", function.getCd22(), element);
        parent.addContent(element);
    }

    /**
     * Builds a JDOM representation of a CoordPolygon2D and adds it to the
     * parent element.
     * 
     * @param name
     *            The name of the element.
     * @param polygon
     *            The CoordPolygon2D to add to the parent.
     * @param parent
     *            The parent element for this child element.
     */
    protected void addCoordPolygon2DElement(String name, CoordPolygon2D polygon, Element parent) {
        if (polygon == null) {
            return;
        }

        Element element = getCaom2Element(name);
        if (!polygon.getVertices().isEmpty()) {
            Element verticesElement = getCaom2Element("vertices");
            element.addContent(verticesElement);
            for (ValueCoord2D vertex : polygon.getVertices()) {
                addValueCoord2DElement("vertex", vertex, verticesElement);
            }
        }
        parent.addContent(element);
    }

    /**
     * Builds a JDOM representation of a CoordRange1D and adds it to the parent
     * element.
     * 
     * @param name
     *            The name of the element.
     * @param range
     *            The CoordRange1D to add to the parent.
     * @param parent
     *            The parent element for this child element.
     */
    protected void addCoordRange1DElement(String name, CoordRange1D range, Element parent) {
        if (range == null) {
            return;
        }

        Element element = getCaom2Element(name);
        addRefCoordElement("start", range.getStart(), element);
        addRefCoordElement("end", range.getEnd(), element);
        parent.addContent(element);
    }

    /**
     * Builds a JDOM representation of a CoordRange2D and adds it to the parent
     * element.
     * 
     * @param name
     *            The name of the element.
     * @param range
     *            The CoordRange2D to add to the parent.
     * @param parent
     *            The parent element for this child element.
     */
    protected void addCoordRange2DElement(String name, CoordRange2D range, Element parent) {
        if (range == null) {
            return;
        }

        Element element = getCaom2Element(name);
        addCoord2DElement("start", range.getStart(), element);
        addCoord2DElement("end", range.getEnd(), element);
        parent.addContent(element);
    }

    /**
     * Builds a JDOM representation of a Dimension2D and adds it to the parent
     * element.
     * 
     * @param name
     *            The name of the element.
     * @param dimension
     *            The Dimension2D to add to the parent.
     * @param parent
     *            The parent element for this child element.
     */
    protected void addDimension2DElement(String name, Dimension2D dimension, Element parent) {
        if (dimension == null) {
            return;
        }

        Element element = getCaom2Element(name);
        addNumberElement("naxis1", dimension.naxis1, element);
        addNumberElement("naxis2", dimension.naxis2, element);
        parent.addContent(element);
    }

    /**
     * Builds a JDOM representation of a RefCoord and adds it to the parent
     * element.
     * 
     * @param name
     *            The name of the element.
     * @param refCoord
     *            The RefCoord to add to the parent.
     * @param parent
     *            The parent element for this child element.
     */
    protected void addRefCoordElement(String name, RefCoord refCoord, Element parent) {
        if (refCoord == null) {
            return;
        }

        Element element = getCaom2Element(name);
        addNumberElement("pix", refCoord.pix, element);
        addNumberElement("val", refCoord.val, element);
        parent.addContent(element);
    }

    /**
     * Builds a JDOM representation of a Slice and adds it to the parent
     * element.
     * 
     * @param name
     *            The name of the element.
     * @param slice
     *            The Slice to add to the parent.
     * @param parent
     *            The parent element for this child element.
     */
    protected void addSliceElement(String name, Slice slice, Element parent) {
        if (slice == null) {
            return;
        }

        Element element = getCaom2Element(name);
        addAxisElement("axis", slice.getAxis(), element);
        addNumberElement("bin", slice.getBin(), element);
        parent.addContent(element);
    }

    /*
     * General methods for adding Elements.
     */

    /**
     * Builds a JDOM representation Element with the given name and sets the
     * text to the given value, then adds the element to the parent.
     * 
     * @param name
     *            The name of the element.
     * @param value
     *            The value for the element.
     * @param parent
     *            The parent element for this child element.
     */
    protected void addElement(String name, String value, Element parent) {
        if (value == null) {
            return;
        }

        Element element = new Element(name, caom2Namespace);
        element.setText(value);
        parent.addContent(element);
    }

    /**
     * Builds a JDOM representation Element with the given name and sets the
     * text to the given value, then adds the element to the parent.
     * 
     * @param name
     *            The name of the element.
     * @param number
     *            The value for the element.
     * @param parent
     *            The parent element for this child element.
     */
    protected void addNumberElement(String name, Number number, Element parent) {
        if (number == null) {
            return;
        }

        Element element = new Element(name, caom2Namespace);
        element.setText(number.toString());
        parent.addContent(element);
    }

    /**
     * Builds a JDOM representation Element with the given name and sets the
     * text to true if the value is true, else sets the text to false, then adds
     * the element to the parent.
     * 
     * @param name
     *            The name of the element.
     * @param value
     *            The boolean value for the element.
     * @param parent
     *            The parent element for this child element.
     */
    protected void addBooleanElement(String name, Boolean value, Element parent) {
        if (value == null) {
            return;
        }

        Element element = new Element(name, caom2Namespace);
        element.setText(value ? "true" : "false");
        parent.addContent(element);
    }

    /**
     * Builds a JDOM representation Element with the given name and sets the
     * text to the given value, then adds the element to the parent.
     * 
     * @param name
     *            The name of the element.
     * @param uri
     *            The URI value for the element.
     * @param parent
     *            The parent element for this child element.
     */
    protected void addURIElement(String name, URI uri, Element parent) {
        if (uri == null) {
            return;
        }

        Element element = new Element(name, caom2Namespace);
        element.setText(uri.toString());
        parent.addContent(element);
    }

    /**
     * Builds a JDOM representation Element with the given name and adds space
     * delimited List values as the text, then adds the element to the parent.
     * 
     * @param name
     *            The name of the element.
     * @param values
     *            The List of Strings for the element.
     * @param parent
     *            The parent element for this child element.
     */
    protected void addStringListElement(String name, Collection<String> values, Element parent) {
        if (values == null || (values.isEmpty() && !writeEmptyCollections)) {
            return;
        }

        Element element = new Element(name, caom2Namespace);
        StringBuilder sb = new StringBuilder();
        for (String value : values) {
            sb.append(value).append(" ");
        }
        element.setText(sb.toString().trim());
        parent.addContent(element);
    }

    protected void addKeywordsElement(Collection<String> values, Element parent) {
        if (values == null || (values.isEmpty() && !writeEmptyCollections)) {
            return;
        }

        Element element = new Element("keywords", caom2Namespace);
        StringBuilder sb = new StringBuilder();
        for (String value : values) {
            Element kw = new Element("keyword", caom2Namespace);
            kw.addContent(value);
            element.addContent(kw);
        }
        parent.addContent(element);
    }

    /**
     * Builds a JDOM representation Element with the given name and adds space
     * delimited List values as the text, then adds the element to the parent.
     * 
     * @param name
     *            The name of the element.
     * @param values
     *            The List of CoordRange1D for the element.
     * @param parent
     *            The parent element for this child element.
     */
    protected void addCoordRange1DListElement(String name, List<CoordRange1D> values, Element parent) {
        if (values == null) {
            return;
        }

        Element element = new Element(name, caom2Namespace);
        for (CoordRange1D range : values) {
            addCoordRange1DElement("range", range, element);
        }
        parent.addContent(element);
    }

    /**
     * Builds a JDOM representation of the Date in IVOA format and adds it to
     * the Observation element.
     * 
     * @param name
     *            The name of the element.
     * @param date
     *            The Date for the element.
     * @param parent
     *            The parent element for this child element.
     * @param dateFormat
     *            IVOA DateFormat.
     */
    protected void addDateElement(String name, Date date, Element parent, DateFormat dateFormat) {
        if (date == null) {
            return;
        }

        Element element = new Element(name, caom2Namespace);
        element.setText(dateFormat.format(date));
        parent.addContent(element);
    }

    private Element getCaom2Element(String name) {
        return new Element(name, caom2Namespace);
    }
}