htsjdk.samtools.reference.FastaReferenceWriter Maven / Gradle / Ivy
/*
* The MIT License
*
* Copyright (c) 2018 The Broad Institute
*
* 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 htsjdk.samtools.reference;
import htsjdk.samtools.SAMSequenceDictionaryCodec;
import htsjdk.samtools.SAMSequenceRecord;
import htsjdk.samtools.util.SequenceUtil;
import htsjdk.utils.ValidationUtils;
import org.apache.commons.compress.utils.CountingOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.HashSet;
import java.util.Set;
/**
* Writes a FASTA formatted reference file.
* In addition it can also compose the index and dictionary files for the newly written reference file.
*
*
* Example:
*
* String[] seqNames = ...;
* byte[][] seqBases = ...;
* ...
* try (final FastaReferenceWriter writer = new FastaReferenceFileWriter(outputFile)) {
* for (int i = 0; i < seqNames.length; i++) {
* writer.startSequence(seqNames[i]).appendBases(seqBases[i]);
* }
* }
*
*
*
* The two main operations that one can invoke on a opened writer is {@link #startSequence} and {@link #appendBases}.
* The former indicates that we are going to append a new sequence to the output and is invoked once per sequence.
* The latter adds bases to the current sequence and can be called as many times as is needed.
*
*
* The writer will make sure that the output adheres to the FASTA reference sequence file format restrictions:
*
* - Sequence names are valid (non-empty, without space/blank, control characters),
* - Sequence description are valid (without control characters),
* - Bases are valid nucleotides or IUPAC redundancy codes and X [ACGTNX...] (lower or uppercase are accepted),
* - Sequence cannot have 0 length,
* - And that each sequence can only appear once in the output
*
*
*/
public final class FastaReferenceWriter implements AutoCloseable {
/**
* Default number of bases per line.
*/
public static final int DEFAULT_BASES_PER_LINE = 60;
/**
* Sequence header start character.
*/
public static final char HEADER_START_CHAR = '>';
/**
* Character used to separate the sequence name and the description if any.
*/
public static final char HEADER_NAME_AND_DESCRIPTION_SEPARATOR = ' ';
/**
* Charset used for all outputs; fixed to UTF-8.
*/
private static final Charset CHARSET = Charset.forName("UTF-8");
/**
* The line separator string.
*/
private static final char LINE_SEPARATOR_CHR = '\n';
/**
* Character used to separate the fields in a index file line.
*/
private static final char INDEX_FIELD_SEPARATOR_CHR = '\t';
/**
* Convenient cached {@code byte[]} representation of the line separator.
*/
private static final byte[] LINE_SEPARATOR = String.valueOf(LINE_SEPARATOR_CHR).getBytes(CHARSET);
/**
* Output stream to the main FASTA output.
*
* We use it also to count the number of bytes so far outputted thus the offset included in
* the index file entry.
*
*/
private final CountingOutputStream fastaStream;
/**
* Writer for the FAI index file.
*
* NOTE: GZI index writing (if necessary) is handled to BlockCompressedOutputStream class and thus is not controlled here.
*/
private final Writer faiIndexWriter;
/**
* Output writer to the output dictionary.
*/
private final Writer dictWriter;
/**
* the md5 digester (or null if not adding md5)
*/
private final MessageDigest md5Digester;
/**
* Output codec for the dictionary.
*/
private final SAMSequenceDictionaryCodec dictCodec;
/**
* Default number of bases per line to be applied unless one is
*/
private final int defaultBasePerLine;
/**
* Records the sequence names that have been already fully appended to this writer.
*/
private final Set sequenceNames = new HashSet<>();
/**
* Bases per line to be applied to the sequence that is been currently appended to the output.
*/
private int currentBasesPerLine;
/**
* Holds the number of bases in the current output line.
*/
private int currentLineBasesCount;
/**
* Holds the number of bases so far appended for the current sequence.
*/
private long currentBasesCount;
/**
* Holds the FASTA output file offset for the current sequence.
*/
private long currentSequenceOffset;
/**
* Holds the name of the sequence that is been appended currently.
*/
private String currentSequenceName;
/**
* Flag indicating whether this writer has been already closed.
*/
private boolean closed;
/**
* Creates a reference FASTA file writer (private...use the builder: {@link FastaReferenceWriterBuilder}.
*
* You can specify a specific output stream to each file: the main fasta output, its index and its dictionary.
* You can only provide a compressed stream to the fastaOutput, and only in the case that an index isn't written.
*
*
*
*
* @param fastaOutput the (uncompressed) output fasta file path.
* @param indexOutput the (uncompressed) output stream to the index file, if requested, {@code null} if none should be generated.
* @param dictOutput the (uncompressed) output stream to the dictFile, if requested, {@code null} if none should be generated.
* @throws IllegalArgumentException if {@code fastaFile} is {@code null} or {@code basesPerLine} is 0 or negative.
*/
FastaReferenceWriter(final int basesPerLine, final boolean addMd5,
final OutputStream fastaOutput,
final OutputStream indexOutput,
final OutputStream dictOutput) {
try {
this.md5Digester = addMd5 ? MessageDigest.getInstance("MD5") : null;
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("Couldn't get md5 algorithm!", e);
}
this.defaultBasePerLine = basesPerLine;
this.fastaStream = new CountingOutputStream(fastaOutput);
this.faiIndexWriter = indexOutput == null ? NullWriter.NULL_WRITER : new OutputStreamWriter(indexOutput, CHARSET);
this.dictWriter = dictOutput == null ? NullWriter.NULL_WRITER : new OutputStreamWriter(dictOutput, CHARSET);
this.dictCodec = new SAMSequenceDictionaryCodec(dictWriter);
this.dictCodec.encodeHeaderLine(false);
}
// checks that a sequence name is valid.
private static void checkSequenceName(final String name) {
ValidationUtils.nonEmpty(name, "Sequence name");
for (int i = 0; i < name.length(); i++) {
final char ch = name.charAt(i);
if (Character.isWhitespace(ch)) {
throw new IllegalArgumentException("the input name contains blank characters: '" + name + "'");
} else if (Character.isISOControl(ch)) {
throw new IllegalArgumentException("the input name contains control characters: '" + name + "'");
}
}
}
private static void checkSequenceBases(final byte[] bases, final int offset, final int length) {
ValidationUtils.nonNull(bases, "input bases");
ValidationUtils.validateArg(bases.length >= offset + length, "Cannot validate bases beyond end of array.");
final int to = offset + length;
for (int i = offset; i < to; i++) {
final byte b = bases[i];
if (!SequenceUtil.isIUPAC(b)) {
throw new IllegalArgumentException("the input sequence contains invalid base calls like: " + (char) b);
}
}
}
private static String checkDescription(final String description) {
if (description == null || description.isEmpty()) {
return "";
}
for (int i = 0; i < description.length(); i++) {
final char c = description.charAt(i);
if (Character.isISOControl(c) && c != '\t') { // tab is the only valid control char in the description.
throw new IllegalArgumentException("the input name contains non-tab control characters: '" +
description + "'");
}
}
return description;
}
/**
* Starts the input of the bases of a new sequence.
*
* This operation automatically closes the previous sequence base input if any.
*
*
* The sequence name cannot contain any blank characters (as determined by {@link Character#isWhitespace(char)}),
* control characters (as determined by {@link Character#isISOControl(char)}) or the the FASTA header start character
* {@value #HEADER_START_CHAR}. It cannot be the empty string either ("").
*
*
* No description is included in the output.
*
*
* The input bases-per-line is set to the default provided at construction or {@link #DEFAULT_BASES_PER_LINE}
* if none was provided.
*
*
* This method cannot be called after the writer has been closed.
*
*
* It also will fail if no base was added to the previous sequence if any.
*
*
* @param sequenceName the name of the new sequence.
* @return this instance.
* @throws IllegalArgumentException if any argument does not comply with requirements listed above or if a sequence
* with the same name has already been added to the writer.
* @throws IllegalStateException if no base was added to the previous sequence or the writer is already closed.
* @throws IOException if such exception is thrown when writing into the output resources.
*/
public FastaReferenceWriter startSequence(final String sequenceName)
throws IOException {
return startSequence(sequenceName, "", defaultBasePerLine);
}
/**
* Starts the input of the bases of a new sequence.
*
* This operation automatically closes the previous sequence base input if any.
*
*
* The sequence name cannot contain any blank characters (as determined by {@link Character#isWhitespace(char)}),
* control characters (as determined by {@link Character#isISOControl(char)}) or the the FASTA header start character
* {@value #HEADER_START_CHAR}. It cannot be the empty string either ("").
*
*
* The input bases-per-line must be 1 or greater.
*
*
* This method cannot be called after the writer has been closed.
*
*
* It also will fail if no base was added to the previous sequence if any.
*
*
* @param sequenceName the name of the new sequence.
* @param basesPerLine number of bases per line for this sequence.
* @return this instance.
* @throws IllegalArgumentException if any argument does not comply with requirements listed above or if a sequence
* with the same name has already been added to the writer.
* @throws IllegalStateException if no base was added to the previous sequence or the writer is already closed.
* @throws IOException if such exception is thrown when writing into the output resources.
*/
public FastaReferenceWriter startSequence(final String sequenceName, final int basesPerLine)
throws IOException {
return startSequence(sequenceName, "", FastaReferenceWriterBuilder.checkBasesPerLine(basesPerLine));
}
/**
* Starts the input of the bases of a new sequence.
*
* This operation automatically closes the previous sequence base input if any.
*
*
* The sequence name cannot contain any blank characters (as determined by {@link Character#isWhitespace(char)}),
* control characters (as determined by {@link Character#isISOControl(char)}) or the the FASTA header start character
* {@value #HEADER_START_CHAR}. It cannot be the empty string either ("").
*
*
* The description cannot contain {@link Character#isISOControl(char)}. If set to {@code null} or the empty
* string ("") no description will be outputted.
*
*
* The input bases-per-line is set to the default provided at construction or {@link #DEFAULT_BASES_PER_LINE}
* if none was provided.
*
*
* This method cannot be called after the writer has been closed.
*
*
* It also will fail if no base was added to the previous sequence if any.
*
*
* @param sequenceName the name of the new sequence.
* @param description optional description for that sequence.
* @return this instance.
* @throws IllegalArgumentException if any argument does not comply with requirements listed above or if a sequence
* with the same name has already been added to the writer.
* @throws IllegalStateException if no base was added to the previous sequence or the writer is already closed.
* @throws IOException if such exception is thrown when writing into the output resources.
*/
public FastaReferenceWriter startSequence(final String sequenceName, final String description)
throws IOException {
return startSequence(sequenceName, description, defaultBasePerLine);
}
/**
* Starts the input of the bases of a new sequence.
*
* This operation automatically closes the previous sequence base input if any.
*
*
* The sequence name cannot contain any blank characters (as determined by {@link Character#isWhitespace(char)}),
* control characters (as determined by {@link Character#isISOControl(char)}) or the the FASTA header start character
* {@value #HEADER_START_CHAR}. It cannot be the empty string either ("").
*
*
* The description cannot contain {@link Character#isISOControl(char)}. If set to {@code null} or the empty
* string ("") no description will be outputted.
*
*
* The input bases-per-line must be 1 or greater.
*
*
* This method cannot be called after the writer has been closed.
*
*
* It also will fail if no base was added to the previous sequence if any.
*
*
* @param sequenceName the name of the new sequence.
* @param description optional description for that sequence.
* @param basesPerLine number of bases per line for this sequence.
* @return this instance.
* @throws IllegalArgumentException if any argument does not comply with requirements listed above.
* @throws IllegalStateException if no base was added to the previous sequence or the writer is already closed of
* the sequence has been already added.
* @throws IOException if such exception is thrown when writing into the output resources.
*/
public FastaReferenceWriter startSequence(final String sequenceName, final String description, final int basesPerLine)
throws IOException {
assertIsNotClosed();
checkSequenceName(sequenceName);
final String nonNullDescription = checkDescription(description);
FastaReferenceWriterBuilder.checkBasesPerLine(basesPerLine);
closeSequence();
if (sequenceNames.contains(sequenceName)) {
throw new IllegalStateException("the input sequence name '" + sequenceName + "' has already been added");
}
currentSequenceName = sequenceName;
currentBasesPerLine = basesPerLine;
final StringBuilder builder = new StringBuilder(sequenceName.length() + nonNullDescription.length() + 2);
builder.append(HEADER_START_CHAR).append(sequenceName);
if (!nonNullDescription.isEmpty()) {
builder.append(HEADER_NAME_AND_DESCRIPTION_SEPARATOR).append(nonNullDescription);
}
fastaStream.write(builder.toString().getBytes(CHARSET));
fastaStream.write(LINE_SEPARATOR);
currentSequenceOffset = fastaStream.getBytesWritten();
if (md5Digester != null) {
md5Digester.reset();
}
return this;
}
private void closeSequence()
throws IOException {
if (currentSequenceName != null) {
if (currentBasesCount == 0) {
throw new IllegalStateException("no base was added");
}
sequenceNames.add(currentSequenceName);
writeIndexEntry();
writeDictEntry();
fastaStream.write(LINE_SEPARATOR);
currentBasesCount = 0;
currentLineBasesCount = 0;
currentSequenceName = null;
}
}
private void writeIndexEntry()
throws IOException {
faiIndexWriter.append(currentSequenceName).append(INDEX_FIELD_SEPARATOR_CHR)
.append(String.valueOf(currentBasesCount)).append(INDEX_FIELD_SEPARATOR_CHR)
.append(String.valueOf(currentSequenceOffset)).append(INDEX_FIELD_SEPARATOR_CHR)
.append(String.valueOf(currentBasesPerLine)).append(INDEX_FIELD_SEPARATOR_CHR)
.append(String.valueOf(currentBasesPerLine + LINE_SEPARATOR.length)).append(LINE_SEPARATOR_CHR);
}
private void writeDictEntry() {
final SAMSequenceRecord samSequenceRecord = new SAMSequenceRecord(currentSequenceName, (int) currentBasesCount);
if (md5Digester != null) {
samSequenceRecord.setMd5(SequenceUtil.md5DigestToString(md5Digester.digest()));
}
dictCodec.encodeSequenceRecord(samSequenceRecord);
}
/**
* Adds bases to current sequence from a {@code byte} array.
*
* @param basesBases String containing the bases to be added.
* string will be interpreted using ascii and will throw if any character is >= 127.
* @return this instance.
* @throws IllegalArgumentException if {@code bases} is {@code null} or
* the input array contains invalid bases (as assessed by: {@link SequenceUtil#isIUPAC(byte)}).
* @throws IllegalStateException if no sequence was started or the writer is already closed.
* @throws IOException if such exception is throw when writing in any of the outputs.
*/
public FastaReferenceWriter appendBases(final String basesBases)
throws IOException {
return appendBases(basesBases.getBytes(StandardCharsets.US_ASCII));
}
/**
* Adds bases to current sequence from a {@code byte} array.
* Will throw if any character is >= 127.
*
* @param bases array containing the bases to be added.
* @return this instance.
* @throws IllegalArgumentException if {@code bases} is {@code null} or
* the input array contains invalid bases (as assessed by: {@link SequenceUtil#isIUPAC(byte)}).
* @throws IllegalStateException if no sequence was started or the writer is already closed.
* @throws IOException if such exception is throw when writing in any of the outputs.
*/
public FastaReferenceWriter appendBases(final byte[] bases)
throws IOException {
return appendBases(bases, 0, bases.length);
}
/**
* Adds bases to current sequence from a range in a {@code byte} array.
* Will throw if any character is >= 127.
*
* @param bases array containing the bases to be added.
* @param offset the position of the first base to add.
* @param length how many bases to be added starting from position {@code offset}.
* @return this instance.
* @throws IllegalArgumentException if {@code bases} is {@code null} or
* {@code offset} and {@code length} do not entail a valid range in {@code bases} or
* that range in {@code base} contain invalid bases (as assessed by: {@link SequenceUtil#isIUPAC(byte)}).
* @throws IllegalStateException if no sequence was started or the writer is already closed.
* @throws IOException if such exception is throw when writing in any of the outputs.
*/
public FastaReferenceWriter appendBases(final byte[] bases, final int offset, final int length)
throws IOException {
assertIsNotClosed();
assertSequenceOpen();
checkSequenceBases(bases, offset, length);
ValidationUtils.validateArg(offset >= 0, "the input offset cannot be negative");
ValidationUtils.validateArg(length >= 0, "the input length must not be negative");
final int to = offset + length;
ValidationUtils.validateArg(to <= bases.length, "the length + offset goes beyond the end of " +
"the input base array: '" + to + "' > '" + bases.length + "'");
int next = offset;
while (next < to) {
if (currentLineBasesCount == currentBasesPerLine) {
fastaStream.write(LINE_SEPARATOR);
currentLineBasesCount = 0;
}
final int nextLength = Math.min(to - next, currentBasesPerLine - currentLineBasesCount);
fastaStream.write(bases, next, nextLength);
if (md5Digester != null) {
md5Digester.update(new String(bases, next, nextLength).toUpperCase().getBytes());
}
currentLineBasesCount += nextLength;
next += nextLength;
}
currentBasesCount += length;
return this;
}
/**
* Appends a new sequence to the output.
*
* This is a convenient short handle for {@code startSequence(name).appendBases(bases)}.
*
*
* The new sequence remains open meaning that additional bases for that sequence can be added with additional calls to {@link #appendBases}.
*
*
* @param sequence a {@link ReferenceSequence} to add.
* @return a reference to this very same writer.
* @throws IOException if such an exception is thrown when actually writing into the output streams/channels.
* @throws IllegalArgumentException if either {@code name} or {@code bases} is {@code null} or contains an invalid value (e.g. unsupported bases or sequence names).
* @throws IllegalStateException if the writer is already closed, a previous sequence (if any was opened) has no base appended to it or a sequence
* with such name was already appended to this writer.
*/
public FastaReferenceWriter addSequence(ReferenceSequence sequence) throws IOException {
return startSequence(sequence.getName()).appendBases(sequence.getBases());
}
/**
* Appends a new sequence to the output with or without a description.
*
* This is a convenient short handle for {@code startSequence(name, description).appendBases(bases)}.
*
*
* A {@code null} or empty ("") description will be ignored (no description will be output).
*
*
* The new sequence remains open meaning that additional bases for that sequence can be added with additional calls to {@link #appendBases}.
*
*
* @param name the name of the new sequence.
* @param bases the (first) bases of the sequence.
* @param description the description for the new sequence.
* @return a reference to this very same writer.
* @throws IOException if such an exception is thrown when actually writing into the output streams/channels.
* @throws IllegalArgumentException if either {@code name} or {@code bases} is {@code null} or contains an invalid value (e.g. unsupported bases or sequence names). Also when
* the {@code description} contains unsupported characters.
* @throws IllegalStateException if the writer is already closed, a previous sequence (if any was opened) has no base appended to it or a sequence
* with such name was already appended to this writer.
*/
public FastaReferenceWriter appendSequence(final String name, final String description, final byte[] bases) throws IOException {
return startSequence(name, description).appendBases(bases);
}
/**
* Appends a new sequence to the output with or without a description and an alternative number of bases-per-line.
*
* This is a convenient short handle for {@code startSequence(name, description, bpl).appendBases(bases)}.
*
*
* A {@code null} or empty ("") description will be ignored (no description will be output).
*
*
* The new sequence remains open meaning that additional bases for that sequence can be added with additional calls to {@link #appendBases}.
*
*
* @param name the name of the new sequence.
* @param bases the (first) bases of the sequence.
* @param description the description for the sequence.
* @param basesPerLine alternative number of bases per line to be used for the sequence.
* @return a reference to this very same writer.
* @throws IOException if such an exception is thrown when actually writing into the output streams/channels.
* @throws IllegalArgumentException if either {@code name} or {@code bases} is {@code null} or contains an invalid value (e.g. unsupported bases or sequence names). Also when the
* {@code description} contains unsupported characters or {@code basesPerLine} is 0 or negative.
* @throws IllegalStateException if the writer is already closed, a previous sequence (if any was opened) has no base appended to it or a sequence
* with such name was already appended to this writer.
*/
public FastaReferenceWriter appendSequence(final String name, final String description, final int basesPerLine, final byte[] bases) throws IOException {
return startSequence(name, description, basesPerLine).appendBases(bases);
}
private void assertSequenceOpen() {
if (currentSequenceName == null) {
throw new IllegalStateException("trying to add bases without starting a sequence");
}
}
private void assertIsNotClosed() {
if (closed) {
throw new IllegalStateException("already closed");
}
}
/**
* Closes this writer flushing all remaining writing operation input the output resources.
*
* Further calls to {@link #appendBases} or {@link #startSequence} will result in an exception.
*
*
* @throws IOException if such exception is thrown when closing output writers and output streams.
* @throws IllegalStateException if closing without writing any sequences or closing when writing a sequence is in progress
*/
@Override
public void close() throws IOException {
if (!closed) {
try {
closeSequence();
if (sequenceNames.isEmpty()) {
throw new IllegalStateException("no sequences were added to the reference");
}
} finally {
closed = true;
fastaStream.close();
faiIndexWriter.close();
dictWriter.close();
}
}
}
/**
* Convenient method to write a FASTA file with a single sequence.
*
* @param whereTo the path to. must not be null.
* @param makeIndex whether the index file should be written at its standard location.
* @param makeDict whether the dictionary file should be written at it standard location.
* @param name the sequence name, cannot contain white space, or control chracter or the header start character.
* @param description the sequence description, can be null or "" if no description.
* @param bases the sequence bases, cannot be {@code null}.
* @throws IOException if such exception is thrown when writing in the output resources.
*/
public static void writeSingleSequenceReference(final Path whereTo, final boolean makeIndex,
final boolean makeDict, final String name,
final String description, final byte[] bases)
throws IOException {
try (final FastaReferenceWriter writer = new FastaReferenceWriterBuilder().setFastaFile(whereTo).setMakeFaiOutput(makeIndex).setMakeDictOutput(makeDict).build()) {
writer.startSequence(name, description);
writer.appendBases(bases);
}
}
/**
* Convenient method to write a FASTA file with a single sequence.
*
* @param whereTo the path to. must not be null.
* @param basesPerLine number of bases per line. must be 1 or greater.
* @param makeIndex whether the index file should be written at its standard location.
* @param makeDict whether the dictionary file should be written at it standard location.
* @param name the sequence name, cannot contain white space, or control chracter or the header start character.
* @param description the sequence description, can be null or "" if no description.
* @param bases the sequence bases, cannot be {@code null}.
* @throws IOException if such exception is thrown when writing in the output resources.
*/
public static void writeSingleSequenceReference(final Path whereTo, final int basesPerLine, final boolean makeIndex,
final boolean makeDict, final String name,
final String description, final byte[] bases)
throws IOException {
try (final FastaReferenceWriter writer = new FastaReferenceWriterBuilder().setBasesPerLine(basesPerLine).setFastaFile(whereTo).setMakeFaiOutput(makeIndex).setMakeDictOutput(makeDict).build()) {
writer.startSequence(name, description);
writer.appendBases(bases);
}
}
private static class NullWriter extends Writer {
@Override
public void write(char[] cbuf, int off, int len) throws IOException {
// no op
}
@Override
public void flush() throws IOException {
// no op
}
@Override
public void close() throws IOException {
// no op
}
private NullWriter() {
}
/**
* The only singleton instance of this class (no need for more!)
*/
public final static NullWriter NULL_WRITER = new NullWriter();
}
}