
uk.ac.starlink.votable.VOTableFitsTableWriter Maven / Gradle / Ivy
package uk.ac.starlink.votable;
import java.io.BufferedWriter;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import uk.ac.starlink.fits.AbstractFitsTableWriter;
import uk.ac.starlink.fits.CardFactory;
import uk.ac.starlink.fits.CardImage;
import uk.ac.starlink.fits.FitsTableSerializer;
import uk.ac.starlink.fits.FitsUtil;
import uk.ac.starlink.fits.ParsedCard;
import uk.ac.starlink.table.StarTable;
import uk.ac.starlink.table.TableSequence;
import uk.ac.starlink.util.DataBufferedOutputStream;
/**
* TableWriter which writes table data into the first extension of a FITS file,
* Unlike {@link uk.ac.starlink.fits.FitsTableWriter} however, the
* primary extension is not left contentless, instead it gets the
* text of a DATA-less VOTable written into it. This VOTable describes
* the metadata of the table.
* Tables stored using this (non-standard) mechanism have all the rich
* metadata associated with VOTables, and benefit from the compactness
* of FITS tables, withouth the considerable disadvantage of being split
* into two files.
*
* @author Mark Taylor (Starlink)
* @since 26 Aug 2004
*/
public abstract class VOTableFitsTableWriter extends AbstractFitsTableWriter {
private VOTableVersion votVersion_;
private static String XML_ENCODING = "UTF-8";
private static Logger logger =
Logger.getLogger( "uk.ac.starlink.votable" );
/**
* Constructor.
*
* @param formatName handler format name
*/
protected VOTableFitsTableWriter( String formatName ) {
super( formatName );
setAllowSignedByte( false );
votVersion_ = VOTableVersion.getDefaultVersion();
}
@Override
public void setAllowSignedByte( boolean allowSignedByte ) {
if ( allowSignedByte ) {
throw new IllegalArgumentException( "Not recommended "
+ "for fits-plus" );
}
super.setAllowSignedByte( allowSignedByte );
}
public void writeStarTables( TableSequence tableSeq, OutputStream out )
throws IOException {
/* Get all the input tables and serializers up front.
* This does have negative implications for scalability
* (can't stream one table at a time), but it's necessary
* to write the header. */
List tableList = new ArrayList();
for ( StarTable table; ( table = tableSeq.nextTable() ) != null; ) {
tableList.add( table );
}
StarTable[] tables = tableList.toArray( new StarTable[ 0 ] );
int ntable = tables.length;
FitsTableSerializer[] fitsers = new FitsTableSerializer[ ntable ];
for ( int i = 0; i < ntable; i++ ) {
fitsers[ i ] = createSerializer( tables[ i ] );
}
/* Prepare destination stream. */
DataBufferedOutputStream dout = new DataBufferedOutputStream( out );
out = null;
/* Write the primary HDU. */
writePrimaryHDU( tables, fitsers, dout );
/* Write the data. */
for ( int i = 0; i < ntable; i++ ) {
writeTableHDU( tables[ i ], fitsers[ i ], dout );
}
/* Tidy up. */
dout.flush();
}
/**
* Sets the version of the VOTable standard to use for encoding metadata
* in the primary HDU.
*
* @param votVersion VOTable version to use
*/
public void setVotableVersion( VOTableVersion votVersion ) {
votVersion_ = votVersion;
}
/**
* Writes the primary HDU for a number of tables.
*
* @param tables array of tables to write
* @param fitsers array of serializers corresponding to tables
* @param strm destination stream
*/
private void writePrimaryHDU( StarTable[] tables,
FitsTableSerializer[] fitsers,
OutputStream out )
throws IOException {
/* Try to write the metadata as VOTable text in the primary HDU. */
Exception thrown = null;
try {
writeVOTablePrimary( tables, fitsers, out );
return;
}
catch ( IOException e ) {
thrown = e;
}
/* But if it fails, just write an empty one. */
assert thrown != null;
logger.log( Level.WARNING,
"Failed to write VOTable metadata to primary HDU",
thrown );
FitsUtil.writeEmptyPrimary( out );
}
/**
* Writes a primary that consists of a byte array holding a
* UTF8-encoded VOTable which holds the table metadata.
*
* @param tables tables to write
* @param fitsers FITS serializers
* @param out destination stream
*/
private void writeVOTablePrimary( StarTable[] tables,
FitsTableSerializer[] fitsers,
OutputStream out )
throws IOException {
/* Get a serializer that knows how to write VOTable metadata for
* this table. */
int ntable = tables.length;
if ( fitsers.length != ntable ) {
throw new IllegalArgumentException( "table/serializer count "
+ "mismatch" );
}
/* Get a buffer to hold the VOTable character data. */
StringWriter textWriter = new StringWriter();
/* Turn it into a BufferedWriter. */
BufferedWriter writer = new BufferedWriter( textWriter );
/* Get an object that knows how to write a VOTable document. */
VOTableWriter votWriter =
new VOTableWriter( (DataFormat) null, false, votVersion_ );
/* Output preamble. */
votWriter.writePreTableXML( writer );
String comment = new StringBuffer()
.append( "" )
.toString();
writer.write( comment );
writer.newLine();
/* Output table elements containing the metadata with the help of
* the VOTable serializer. */
for ( int i = 0; i < ntable; i++ ) {
StarTable table = tables[ i ];
FitsTableSerializer fitser = fitsers[ i ];
VOSerializer voser =
VOSerializer.makeFitsSerializer( tables[ i ], fitsers[ i ],
votVersion_ );
voser.writePreDataXML( writer );
writer.write( "" );
writer.newLine();
voser.writePostDataXML( writer );
}
/* Output trailing tags and flush. */
votWriter.writePostTableXML( writer );
writer.flush();
/* Get a byte array containing the VOTable text. */
byte[] textBytes = textWriter.getBuffer().toString()
.getBytes( XML_ENCODING );
int nbyte = textBytes.length;
/* Prepare and write a FITS header describing the character data. */
List cards = new ArrayList<>();
CardFactory cf = CardFactory.STRICT;
cards.addAll( Arrays.asList( new CardImage[] {
cf.createLogicalCard( "SIMPLE", true, "Standard FITS format" ),
cf.createIntegerCard( "BITPIX", 8, "Character data" ),
cf.createIntegerCard( "NAXIS", 1, "Text string" ),
cf.createIntegerCard( "NAXIS1", nbyte, "Number of characters" ),
} ) );
cards.addAll( Arrays.asList( getCustomPrimaryHeaderCards() ) );
cards.add( cf.createLogicalCard( "EXTEND", true,
"There are standard extensions" ) );
String plural = ntable == 1 ? "" : "s";
String[] comments = new String[] {
" ",
"The data in this primary HDU consists of bytes which",
"comprise a VOTABLE document.",
"The VOTable describes the metadata of the table" + plural
+ " contained",
"in the following BINTABLE extension" + plural + ".",
"Such a BINTABLE extension can be used on its own as a perfectly",
"good table, but the information from this HDU may provide some",
"useful additional metadata.",
( ntable == 1 ? "There is one following BINTABLE."
: "There are " + ntable + " following BINTABLEs." ),
};
for ( int i = 0; i < comments.length; i++ ) {
cards.add( cf.createCommentCard( comments[ i ] ) );
}
cards.add( cf.createIntegerCard( "NTABLE", ntable,
"Number of following BINTABLE HDUs" ));
cards.add( CardFactory.END_CARD );
assert primaryHeaderOK( cards.toArray( new CardImage[ 0 ] ) );
FitsUtil.writeHeader( cards.toArray( new CardImage[ 0 ] ), out );
/* Write the character data itself. */
out.write( textBytes );
/* Write padding to the end of the FITS block. */
int partial = textBytes.length % FitsUtil.BLOCK_LENG;
if ( partial > 0 ) {
int pad = FitsUtil.BLOCK_LENG - partial;
out.write( new byte[ pad ] );
}
}
/**
* Returns implementation-specific header cards to be added
* to the Primary HDU of FITS files written by this writer.
*
* @return header cards
*/
protected abstract CardImage[] getCustomPrimaryHeaderCards();
/**
* Performs assertion-type checks on a primary HDU header written by
* an this object.
*
* @param hdr header to check
* @return true iff the header looks OK
*/
private boolean primaryHeaderOK( CardImage[] cards ) {
try {
ByteArrayOutputStream bout = new ByteArrayOutputStream();
FitsUtil.writeHeader( cards, bout );
bout.close();
return isMagic( bout.toByteArray() );
}
catch ( IOException e ) {
assert false;
return false;
}
}
/**
* Determines whether a given byte buffer looks like it contains
* the start of a primary header written by this writer.
* Calls the protected
* {@link #isMagic(int,java.lang.String,java.lang.Object)} method.
*
* @param buffer start of a file
* @return true iff buffer
looks like it contains a
* file written by this handler
*/
public boolean isMagic( byte[] buffer ) {
final int ntest = 6;
if ( buffer.length < ntest * 80 ) {
return false;
}
byte[] cbuf = new byte[ 80 ];
for ( int il = 0; il < ntest; il++ ) {
System.arraycopy( buffer, il * 80, cbuf, 0, 80 );
ParsedCard> card = FitsUtil.parseCard( cbuf );
if ( ! isMagic( il, card.getKey(), card.getValue() ) ) {
return false;
}
}
return true;
}
/**
* Tests a header card to see if it looks like part of the magic number
* for the format written by this handler.
* The VOTableFitsTableWriter
implementation tests that
* the first four cards read:
*
* SIMPLE = T
* BITPIX = 8
* NAXIS = 1
* NAXIS1 = ???
*
* Subclasses may override this to add tests for later cards
* (as provided by {@link #getCustomPrimaryHeaderCards}).
*
* @param icard 0-based card index
* @param key card name
* @param value card value
* @return true iff the presented card is one that could have been
* written by this writer
*/
protected boolean isMagic( int icard, String key, Object value ) {
switch ( icard ) {
case 0:
return "SIMPLE".equals( key ) && Boolean.TRUE.equals( value );
case 1:
return "BITPIX".equals( key )
&& value instanceof Number
&& ((Number) value).intValue() == 8;
case 2:
return "NAXIS".equals( key )
&& value instanceof Number
&& ((Number) value).intValue() == 1;
case 3:
return "NAXIS1".equals( key );
default:
return true;
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy