/**
* Copyright Intellectual Reserve, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.gedcomx.util;
import org.gedcomx.Gedcomx;
import org.gedcomx.records.RecordSet;
import org.gedcomx.rt.GedcomxConstants;
import javax.xml.bind.*;
import javax.xml.namespace.QName;
import javax.xml.stream.XMLOutputFactory;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamWriter;
import java.io.IOException;
import java.io.OutputStream;
/**
* Class for streaming a RecordSet to an OutputStream in XML as records are being added.
*
* User: Randy Wilson
* Date: 12/4/13
*/
public class RecordSetWriter {
Marshaller marshaller;
private XMLStreamWriter xmlWriter;
// Metadata, if any
private Gedcomx metadata;
// Flag for whether any records have been written so far
private boolean wroteRecords = false;
// Flag for whether the metadata was already written before the records.
private boolean wroteMetadata = false;
// Stream to write data to
private OutputStream outputStream;
/**
* Constructor. Prepares to write GedcomX document records to the given output stream (which may well be a
* GZIPOutputStream), so that only one such document needs to be fully instantiated in memory at once.
* @param outputStream - OutputStream to write XML to.
*/
public RecordSetWriter(OutputStream outputStream) {
this(outputStream, true);
}
/**
* Constructor. Prepares to write GedcomX document records to the given output stream (which may well be a
* GZIPOutputStream), so that only one such document needs to be fully instantiated in memory at once.
* @param outputStream - OutputStream to write XML to.
* @param shouldFilter - Flag for whether to use a CleanXMLStreamWriter to convert invalid XML characters
* (such as a vertical tab) into the Unicode REPLACEMENT_CHARACTER (0xFFFD), so that
* the XML will unmarshal without throwing an exception.
*/
protected RecordSetWriter(OutputStream outputStream, boolean shouldFilter) {
try {
marshaller = JAXBContext.newInstance(Gedcomx.class, RecordSet.class).createMarshaller();
marshaller.setProperty(Marshaller.JAXB_FRAGMENT, true);
this.outputStream = outputStream;
xmlWriter = XMLOutputFactory.newFactory().createXMLStreamWriter(outputStream, "UTF-8");
if (shouldFilter) {
// Use a CleanXMLStreamWriter to avoid illegal XML characters in the marshalled output, such as a vertical tab character.
xmlWriter = new CleanXMLStreamWriter(xmlWriter);
}
xmlWriter.setDefaultNamespace(GedcomxConstants.GEDCOMX_NAMESPACE);
xmlWriter.writeStartDocument();
xmlWriter.writeStartElement(GedcomxConstants.GEDCOMX_NAMESPACE, "records");
xmlWriter.writeNamespace("", GedcomxConstants.GEDCOMX_NAMESPACE);
}
catch (XMLStreamException e) {
throw new RuntimeException(e);
}
catch (PropertyException e) {
throw new RuntimeException(e);
}
catch (JAXBException e) {
throw new RuntimeException(e);
}
}
/**
* Write the given record to the underlying byte-level (and possibly GZipped) output stream.
* @param record - GedcomX document to add as a 'record' to the RecordSet OutputStream.
* @throws JAXBException If there's a problem with the XML.
*/
public synchronized void writeRecord(Gedcomx record) throws JAXBException {
writeRecord(record, "record");
wroteRecords = true;
}
/**
* Set the 'metadata' document, which will be written after all of the records. Often used for the collection-level
* information such as collection source descriptions, record descriptors, etc. May describe a collection that
* goes beyond the records contained within this same GedcomX RecordSet.
* @param metadata - GedcomX document with group-level information.
* @throws JAXBException If there's a problem with the XML.
*/
public synchronized void setMetadata(Gedcomx metadata) throws JAXBException {
if (wroteMetadata) {
throw new IllegalStateException("Already wrote metadata to stream. Can't change it now.");
}
if (wroteRecords) {
// Already wrote some records, so hold onto the metadata document until the end.
this.metadata = metadata;
}
else {
// Haven't written records yet, so write metadata immediately, at the top of the document, for better readability.
// (Don't set this.metadata, because it would waste memory, and we need it to be null during close() so it doesn't get written again.
writeRecord(metadata, "metadata");
wroteMetadata = true;
}
}
private void writeRecord(Gedcomx record, String label) throws JAXBException {
marshaller.marshal(new JAXBElement<Gedcomx>(new QName(GedcomxConstants.GEDCOMX_NAMESPACE, label), Gedcomx.class, record), xmlWriter);
}
/**
* Finish writing the file, including metadata (if set), and the closing tag. Closes the writers and output stream.
* @throws IOException If there's an I/O problem.
*/
public synchronized void close() throws IOException {
try {
if (!wroteMetadata && metadata != null) {
writeRecord(metadata, "metadata");
}
xmlWriter.writeEndElement();
xmlWriter.close();
outputStream.close();
}
catch (XMLStreamException e) {
throw new RuntimeException(e);
}
catch (JAXBException e) {
throw new RuntimeException(e);
}
}
}