All Downloads are FREE. Search and download functionalities are using the official Maven repository.

uk.ac.starlink.votable.VOSerializer Maven / Gradle / Ivy

package uk.ac.starlink.votable;

import java.io.BufferedWriter;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.lang.reflect.Array;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.TreeSet;
import java.util.concurrent.atomic.AtomicLong;
import java.util.logging.Logger;
import uk.ac.starlink.fits.FitsUtil;
import uk.ac.starlink.fits.FitsTableSerializer;
import uk.ac.starlink.fits.FitsTableSerializerConfig;
import uk.ac.starlink.fits.FitsTableWriter;
import uk.ac.starlink.fits.StandardFitsTableSerializer;
import uk.ac.starlink.fits.WideFits;
import uk.ac.starlink.table.ColumnInfo;
import uk.ac.starlink.table.DefaultValueInfo;
import uk.ac.starlink.table.DescribedValue;
import uk.ac.starlink.table.RowSequence;
import uk.ac.starlink.table.StarTable;
import uk.ac.starlink.table.Tables;
import uk.ac.starlink.table.ValueInfo;
import uk.ac.starlink.table.WrapperStarTable;
import uk.ac.starlink.util.BufferedBase64OutputStream;
import uk.ac.starlink.util.DataBufferedOutputStream;
import uk.ac.starlink.util.IntList;
import uk.ac.starlink.votable.datalink.ServiceDescriptor;
import uk.ac.starlink.votable.datalink.ServiceParam;

/**
 * Class which knows how to serialize a table's fields and data to 
 * VOTable elements.  For writing a full VOTable document
 * which contains a single table the {@link VOTableWriter} 
 * class may be more convenient, but 
 * this class can be used in a more flexible way, by writing only
 * the elements which are required.
 *
 * 

Obtain an instance of this class using the {@link #makeSerializer} * method. * * @author Mark Taylor (Starlink) */ public abstract class VOSerializer { private final StarTable table_; private final DataFormat format_; private final List paramList_; private final String ucd_; private final String utype_; private final String description_; private final ServiceDescriptor[] servDescrips_; final Map coosysMap_; final Map timesysMap_; private boolean isCompact_; final static Logger logger = Logger.getLogger( "uk.ac.starlink.votable" ); private static final AtomicLong idSeq_ = new AtomicLong(); private static final String NL_STRING = getNewline(); private static final byte[] NL_BYTES = toAsciiBytes( NL_STRING ); private static final byte[] LT_BYTES = toAsciiBytes( "<" ); private static final byte[] GT_BYTES = toAsciiBytes( ">" ); private static final byte[] AMP_BYTES = toAsciiBytes( "&" ); /** * Constructs a new serializer which can write a given StarTable. * * @param table the table to write * @param format the data format being used * @param version output VOTable version */ private VOSerializer( StarTable table, DataFormat format, VOTableVersion version ) { table_ = table; format_ = format; /* Doctor the table's parameter list. Take out items which are * output specially so that only the others get output as PARAM * elements. */ paramList_ = new ArrayList(); String description = null; String ucd = null; String utype = null; List sdList = new ArrayList(); for ( DescribedValue dval : table.getParameters() ) { ValueInfo pinfo = dval.getInfo(); String pname = pinfo.getName(); Class pclazz = pinfo.getContentClass(); Object value = dval.getValue(); if ( pname != null && pclazz != null ) { if ( pname.equalsIgnoreCase( "description" ) && pclazz == String.class ) { description = (String) value; } else if ( pname.equals( VOStarTable.UCD_INFO.getName() ) && pclazz == String.class ) { ucd = (String) value; } else if ( pname.equals( VOStarTable.UTYPE_INFO.getName() ) && pclazz == String.class ) { utype = (String) value; } else if ( ServiceDescriptor.class.isAssignableFrom( pclazz ) ) { if ( value instanceof ServiceDescriptor ) { sdList.add( (ServiceDescriptor) value ); } } else { paramList_.add( dval ); } } } description_ = description; ucd_ = ucd; utype_ = utype; servDescrips_ = sdList.toArray( new ServiceDescriptor[ 0 ] ); /* Get a base identifier that can be used to prepend to XML ID values. * As long as this is unique per output XML document, * ID namespace clashes can be avoided. * We do it here by incrementing a static variable. * That is not absolutely 100% bulletproof, but it will give * a value that's unique per JVM, so as long as you're not using * multiple JVMs to put together a single VOTable document, * (or, more likely, combining generated XML with unedited XML * pulled in from a previously generated VOTable document) * you should be OK. * It would be possible to make this approach more robust * by initialising the idSeq_ variable with some kind of * pseudo-random value. */ String baseId = "t" + Long.toString( idSeq_.incrementAndGet() ); /* Prepare COOSYS and TIMESYS elements. Identify the materially * different *SYS elements that will be required, and store them * as keys in a map, with values that are newly constructed but * unique-per-JVM identifiers, for later use. */ coosysMap_ = new LinkedHashMap(); timesysMap_ = new LinkedHashMap(); int ncol = table.getColumnCount(); int ics = 0; int its = 0; List infos = new ArrayList(); for ( int ic = 0; ic < ncol; ic++ ) { infos.add( table.getColumnInfo( ic ) ); } for ( DescribedValue dval : table.getParameters() ) { infos.add( dval.getInfo() ); } for ( ValueInfo info : infos ) { MetaEl coosys = getCoosys( info ); if ( coosys != null && ! coosysMap_.containsKey( coosys ) ) { String id = baseId + "-coosys-" + ++ics; coosysMap_.put( coosys, id ); } if ( version.allowTimesys() ) { MetaEl timesys = getTimesys( info ); if ( timesys != null && ! timesysMap_.containsKey( timesys ) ) { String id = baseId + "-timesys-" + ++its; timesysMap_.put( timesys, id ); } } } } /** * Returns the data format which this object can serialize to. * * @return output format */ public DataFormat getFormat() { return format_; } /** * Returns the table object which this object can serialize. * * @return table to write */ public StarTable getTable() { return table_; } /** * Controls whitespace formatting for TABLEDATA format; * no effect for other DataFormats. * * @param isCompact if true, add whitespace round TR/TD elements */ public void setCompact( boolean isCompact ) { isCompact_ = isCompact; } /** * Indicates compactness of whitespace formatting for TABLEDATA format; * no effect for other DataFormats. * * @return if true, add whitespace round TR/TD elements */ public boolean isCompact() { return isCompact_; } /** * Writes the FIELD headers corresponding to this table on a given writer. * * @param writer destination stream */ public abstract void writeFields( BufferedWriter writer ) throws IOException; /** * Writes this serializer's table data as a self-contained * <DATA> element. * If this serializer's format is binary (non-XML) the bytes * will get written base64-encoded into a STREAM element. * * @param writer destination stream */ public abstract void writeInlineDataElement( BufferedWriter writer ) throws IOException; /** * Writes this serializer's table data as a self-contained * <DATA> element to an output stream using UTF8 encoding. * This may be substantially faster than the otherwise equivalent * {@link #writeInlineDataElement(java.io.BufferedWriter)} method. * * @param out output stream */ public abstract void writeInlineDataElementUTF8( OutputStream out ) throws IOException; /** * Writes this serializer's table data to a <DATA> element * containing a <STREAM> element which references an external * data source (optional method). * The binary data itself will be written to an * output stream supplied separately (it will not be inline). * If this serializer's format is not binary (i.e. if it's TABLEDATA) * an UnsupportedOperationException will be thrown. * * @param xmlwriter destination stream for the XML output * @param href URL for the external stream (output as the href * attribute of the written <STREAM> element) * @param streamout destination stream for the binary table data */ public abstract void writeHrefDataElement( BufferedWriter xmlwriter, String href, OutputStream streamout ) throws IOException; /** * Writes this serializer's table as a complete TABLE element. * If this serializer's format is binary (non-XML) the bytes * will get written base64-encoded into a STREAM element. * * @param writer destination stream */ public void writeInlineTableElement( BufferedWriter writer ) throws IOException { writePreDataXML( writer ); writeInlineDataElement( writer ); writePostDataXML( writer ); } /** * Writes this serializer's table as a complete TABLE element * to an output stream using UTF8 encoding. * If this serializer's format is binary (non-XML) the bytes * will get written base64-encoded into a STREAM element. * *

This may be substantially faster than the otherwise equivalent * {@link #writeInlineTableElement(java.io.BufferedWriter)} method. * * @param out output stream */ public void writeInlineTableElementUTF8( OutputStream out ) throws IOException { BufferedWriter bw = new BufferedWriter( new OutputStreamWriter( out, StandardCharsets.UTF_8 ) ); writePreDataXML( bw ); bw.flush(); writeInlineDataElementUTF8( out ); writePostDataXML( bw ); bw.flush(); } public void writeHrefTableElement( BufferedWriter xmlwriter, String href, OutputStream streamout ) throws IOException { writePreDataXML( xmlwriter ); writeHrefDataElement( xmlwriter, href, streamout ); writePostDataXML( xmlwriter ); } /** * Writes any PARAM and INFO elements associated with this serializer's * table. These should generally go in the TABLE element. * * @param writer destination stream */ public void writeParams( BufferedWriter writer ) throws IOException { for ( DescribedValue param : paramList_ ) { ValueInfo pinfo0 = param.getInfo(); DefaultValueInfo pinfo = new DefaultValueInfo( pinfo0 ); Object pvalue = param.getValue(); /* Adjust the info so that its dimension sizes are fixed, * and matched to the sizes of the actual value. * This might make it easier to write or read. */ if ( pinfo.isArray() ) { int[] shape = pinfo.getShape(); if ( shape != null && shape.length > 0 && shape[ shape.length - 1 ] < 0 && pvalue != null && pvalue.getClass().isArray() ) { long block = 1; for ( int idim = 0; idim < shape.length - 1 && block >= 1; idim++ ) { block *= shape[ idim ]; } int leng = Array.getLength( pvalue ); if ( block <= Integer.MAX_VALUE && leng % block == 0 ) { shape[ shape.length - 1 ] = leng / (int) block; pinfo.setShape( shape ); } } } if ( String.class.equals( pinfo.getContentClass() ) && pinfo.getElementSize() < 0 && pvalue instanceof String ) { pinfo.setElementSize( ((String) pvalue).length() ); } if ( String[].class.equals( pinfo.getContentClass() ) && pinfo.getElementSize() < 0 && pvalue instanceof String[] ) { int leng = 0; String[] strs = (String[]) pvalue; for ( int is = 0; is < strs.length; is++ ) { if ( strs[ is ] != null ) { leng = Math.max( leng, strs[ is ].length() ); } } pinfo.setElementSize( leng ); } /* Adjust the info so that its nullability is set from the data. */ pinfo.setNullable( Tables.isBlank( pvalue ) ); /* Try to write it as a typed PARAM element. */ Encoder encoder = Encoder.getEncoder( pinfo, false, false ); if ( encoder != null ) { String valtext = encoder.encodeAsText( pvalue ); String content = encoder.getFieldContent(); Map attMap = new LinkedHashMap(); attMap.putAll( getFieldAttributes( encoder, coosysMap_, timesysMap_ ) ); attMap.put( "value", valtext ); writer.write( " 0 ) { writer.write( ">" ); writer.write( content ); writer.newLine(); writer.write( "" ); } else { writer.write( "/>" ); } writer.newLine(); } /* If it's a URL write it as a LINK. */ else if ( pvalue instanceof URL ) { writer.write( "" ); writer.newLine(); } /* If it's of a funny type, just try to write it as an INFO. */ else { writer.write( "" ); writer.newLine(); } } } /** * Writes any DESCRIPTION element associated with this serializer's table. * This should generally go just inside the TABLE element itself. * If there's no suitable description text, nothing will be written. * * @param writer destination stream */ public void writeDescription( BufferedWriter writer ) throws IOException { if ( description_ != null && description_.trim().length() > 0 ) { writer.write( "" ); writer.newLine(); writer.write( formatText( description_.trim() ) ); writer.newLine(); writer.write( "" ); writer.newLine(); } } /** * Writes the service descriptor parameters of this serializer's table * as a sequence of zero or more RESOURCE elements. * Each has attributes type="meta" and utype="adhoc:service". * * @param writer destination stream */ public void writeServiceDescriptors( BufferedWriter writer ) throws IOException { for ( ServiceDescriptor sd : servDescrips_ ) { writeServiceDescriptor( writer, sd ); } } /** * Writes a service descriptor object as a RESOURCE element with * utype="adhoc:service". * * @param writer destination stream * @param sdesc service descriptor object * @see DataLink-1.0, sec 4 */ private void writeServiceDescriptor( BufferedWriter writer, ServiceDescriptor sdesc ) throws IOException { String sdId = sdesc.getDescriptorId(); String sdName = sdesc.getName(); String sdDescription = sdesc.getDescription(); StringBuffer rtag = new StringBuffer() .append( " 0 ) { rtag.append( formatAttribute( "ID", sdId ) ); } rtag.append( ">" ); writer.write( rtag.toString() ); writer.newLine(); if ( sdDescription != null ) { writer.write( " " + formatText( sdDescription.trim() ) + "" ); writer.newLine(); } writeStringParam( writer, "accessURL", sdesc.getAccessUrl() ); writeStringParam( writer, "standardID", sdesc.getStandardId() ); writeStringParam( writer, "resourceIdentifier", sdesc.getResourceIdentifier() ); ServiceParam[] sdParams = sdesc.getInputParams(); if ( sdParams.length > 0 ) { writer.write( " " ); writer.newLine(); for ( ServiceParam sdParam : sdParams ) { writeServiceParam( writer, sdParam ); } writer.write( " " ); writer.newLine(); } writer.write( "" ); writer.newLine(); } /** * Writes a PARAM element with a given value, if the value is not blank. * If the value is null or the empty string, no output is written. * * @param writer destination stream * @param pname parameter name * @parma pvalue parameter value */ private void writeStringParam( BufferedWriter writer, String pname, String pvalue ) throws IOException { if ( pvalue != null && pvalue.length() > 0 ) { StringBuffer sbuf = new StringBuffer() .append( " " ); writer.write( sbuf.toString() ); writer.newLine(); } } /** * Serialises a ServiceParam object as a PARAM element, * assumed to be within an inputParams GROUP. * * @param writer destination stream * @param param service parameter object */ private void writeServiceParam( BufferedWriter writer, ServiceParam param ) throws IOException { int[] arraysize = param.getArraysize(); String name = param.getName(); String datatype = param.getDatatype(); String value = param.getValue(); Map atts = new LinkedHashMap(); atts.put( "name", name == null ? "??" : name ); atts.put( "datatype", datatype == null ? "char" : datatype ); if ( arraysize != null && arraysize.length > 0 ) { atts.put( "arraysize", DefaultValueInfo.formatShape( arraysize ) ); } atts.put( "value", value == null ? "" : value ); atts.put( "unit", param.getUnit() ); atts.put( "ucd", param.getUcd() ); atts.put( "utype", param.getUtype() ); atts.put( "xtype", param.getXtype() ); atts.put( "ref", param.getRef() ); for ( String aname : new String[] { "unit", "ucd", "utype", "xtype", "ref", } ) { String aval = atts.get( aname ); if ( aval == null || aval.length() == 0 ) { atts.remove( aname ); } } writer.write( " " ); writer.newLine(); String descrip = param.getDescription(); if ( descrip != null && descrip.trim().length() > 0 ) { writer.write( " " + formatText( descrip ) + "" ); writer.newLine(); } String[] options = param.getOptions(); String[] minmax = param.getMinMax(); String min = minmax == null ? null : minmax[ 0 ]; String max = minmax == null ? null : minmax[ 1 ]; if ( min != null || max != null || options != null ) { writer.write( " " ); writer.newLine(); if ( min != null ) { writer.write( " " ); writer.newLine(); } if ( max != null ) { writer.write( " " ); writer.newLine(); } if ( options != null ) { for ( String opt : options ) { writer.write( " " ); writer.newLine(); } } writer.write( " " ); writer.newLine(); } writer.write( " " ); writer.newLine(); } /** * Outputs the TABLE element start tag and all of its content before * the DATA element. * Other items legal where a TABLE can appear may be prepended * if required. * * @param writer output stream */ public void writePreDataXML( BufferedWriter writer ) throws IOException { /* If we have COOSYS or TIMESYS elements, write them. * The schema constrains where these are allowed to go. * Although in some cases they * can go on their own before a TABLE element, depending on what * comes before that might not be allowed. It's always safe * (at least in VOTable 1.2+, though not 1.1) to wrap them * in their own RESOURCE at the same level as a TABLE. */ if ( coosysMap_.size() + timesysMap_.size() > 0 ) { writer.write( "" ); writer.newLine(); Map metamap = new LinkedHashMap(); metamap.putAll( coosysMap_ ); metamap.putAll( timesysMap_ ); for ( Map.Entry entry : metamap.entrySet() ) { MetaEl meta = entry.getKey(); String id = entry.getValue(); writer.write( " " + meta.toXml( id ) ); writer.newLine(); } writer.write( "" ); writer.newLine(); } /* Output TABLE element start tag. */ writer.write( " 0 ) { writer.write( formatAttribute( "name", tname.trim() ) ); } /* Write the number of rows if we know it. */ long nrow = getTable().getRowCount(); if ( nrow > 0 ) { writer.write( formatAttribute( "nrows", Long.toString( nrow ) ) ); } /* Write UCD and utype information if we have it. */ if ( ucd_ != null ) { writer.write( formatAttribute( "ucd", ucd_ ) ); } if ( utype_ != null ) { writer.write( formatAttribute( "utype", utype_ ) ); } /* Close TABLE element start tag. */ writer.write( ">" ); writer.newLine(); /* Output a DESCRIPTION element if we have something suitable. */ writeDescription( writer ); /* Output table parameters as PARAM elements. */ writeParams( writer ); /* Output FIELD headers. */ writeFields( writer ); } /** * Outputs any content of the TABLE element following the DATA element * and the TABLE end tag. * * @param writer output stream */ public void writePostDataXML( BufferedWriter writer ) throws IOException { writer.write( "" ); writer.newLine(); writeServiceDescriptors( writer ); } /** * Turns a name,value pair into an attribute assignment suitable for * putting in an XML start tag. * The resulting string starts with, but does not end with, whitespace. * Any necessary escaping of the strings is taken care of. * * @param name the attribute name * @param value the attribute value * @return string of the form ' name="value"' */ public static String formatAttribute( String name, String value ) { int vleng = value.length(); StringBuffer buf = new StringBuffer( name.length() + vleng + 4 ); buf.append( ' ' ) .append( name ) .append( '=' ) .append( '"' ); /* Assemble the string, counting the number of single and double * quote substitutions required. */ int nquot = 0; int napos = 0; for ( int i = 0; i < vleng; i++ ) { char c = value.charAt( i ); switch ( c ) { case '<': buf.append( "<" ); break; case '>': buf.append( ">" ); break; case '&': buf.append( "&" ); break; case '"': buf.append( """ ); nquot++; break; case '\'': napos++; buf.append( ensureLegalXml( c ) ); break; default: buf.append( ensureLegalXml( c ) ); } } buf.append( '"' ); /* We're probably done; most likely there were no single or double * quotes. But if it turns out that the output had lots of * double quotes and not so many single quotes, redo it with * single quotes on the outside to get a tidier result. */ if ( nquot <= napos ) { return buf.toString(); } else { buf.setLength( 0 ); buf.append( ' ' ) .append( name ) .append( '=' ) .append( '\'' ); for ( int i = 0; i < vleng; i++ ) { char c = value.charAt( i ); switch ( c ) { case '<': buf.append( "<" ); break; case '>': buf.append( ">" ); break; case '&': buf.append( "&" ); break; case '\'': buf.append( "'" ); break; default: buf.append( ensureLegalXml( c ) ); } } buf.append( '\'' ); return buf.toString(); } } /** * Performs necessary special character escaping for text which * will be written as XML CDATA. * * @param text the input text * @return text but with XML special characters escaped */ public static String formatText( String text ) { int leng = text.length(); StringBuffer sbuf = new StringBuffer( leng ); for ( int i = 0; i < leng; i++ ) { char c = text.charAt( i ); switch ( c ) { case '<': sbuf.append( "<" ); break; case '>': sbuf.append( ">" ); break; case '&': sbuf.append( "&" ); break; default: sbuf.append( ensureLegalXml( c ) ); } } return sbuf.toString(); } /** * Writes the content of an arbitrary string as XML content to a * given output stream in UTF8 representation. * Unsafe characters '<', '>' and '&' * will be escaped appropriately, * and characters that are not legal XML will be substituted. * * @param txt text * @param out destination stream */ private static void writeEscapedTextUTF8( String txt, DataBufferedOutputStream out ) throws IOException { int leng = txt.length(); for ( int i = 0; i < leng; i++ ) { char c = txt.charAt( i ); switch ( c ) { case '<': out.write( LT_BYTES ); break; case '>': out.write( GT_BYTES ); break; case '&': out.write( AMP_BYTES ); break; default: out.writeCharUTF8( ensureLegalXml( c ) ); } } } /** * Returns the content of a string as an array of ASCII bytes. * The upper 8 bits are ignored. * Should not be called on text that may contain strange characters. * * @param txt text * @return byte array */ private static byte[] toAsciiBytes( String txt ) { int leng = txt.length(); byte[] buf = new byte[ leng ]; for ( int i = 0; i < leng; i++ ) { buf[ i ] = (byte) txt.charAt( i ); } return buf; } /** * Returns a string containing the platform-specific newline string. * * @return newline string, one or two characters usually */ private static String getNewline() { try { return System.getProperty( "line.separator" ); } catch ( Throwable e ) { return "\n"; } } /** * Returns a legal XML character corresponding to an input character. * Certain characters are simply illegal in XML (regardless of encoding). * If the input character is legal in XML, it is returned; * otherwise some other weird but legal character * (currently the inverted question mark, "\u00BF") is returned instead. * * @param c input character * @return legal XML character, c if possible */ public static char ensureLegalXml( char c ) { return ( ( c >= '\u0020' && c <= '\uD7FF' ) || ( c >= '\uE000' && c <= '\uFFFD' ) || ( ((int) c) == 0x09 || ((int) c) == 0x0A || ((int) c) == 0x0D ) ) ? c : '\u00BF'; } /** * Turns a Map of name,value pairs into a string of attribute * assignments suitable for putting in an XML start tag. * The resulting string starts with, but does not end with, whitespace. * Any necessary escaping of the strings is taken care of. * * @param atts Map of name,value pairs * @return a string of name="value" assignments */ private static String formatAttributes( Map atts ) { StringBuffer sbuf = new StringBuffer(); for ( String attname : new TreeSet( atts.keySet() ) ) { String attval = atts.get( attname ); sbuf.append( formatAttribute( attname, attval ) ); } return sbuf.toString(); } /** * Writes a FIELD element to a writer. * * @param content text content of the element, if any * @param attributes a name-value map of attributes * @param writer destination stream */ private static void writeFieldElement( BufferedWriter writer, String content, Map attributes ) throws IOException { writer.write( " 0 ) { writer.write( '>' ); writer.write( content ); writer.newLine(); writer.write( "" ); } else { writer.write( "/>" ); } writer.newLine(); } /** * Applies miscellaneous preparation steps to a table that will * have a VOSerializer built from it. * * @param table table for preparation * @param magicNulls whether magic null values may be required; * if true, then NULL_VALUE_INFO entries are added to their * auxiliary metadata where required (nullable scalar integer * columns), * if false any such entries are removed * @param allowXtype whether xtype attributes are permitted in the output; * if not, any keys which might give rise to them in the * serialization are removed * @return prepared table (possibly the same as input). */ private static StarTable prepareForSerializer( StarTable table, boolean magicNulls, boolean allowXtype ) { ValueInfo badKey = Tables.NULL_VALUE_INFO; ValueInfo ubyteKey = Tables.UBYTE_FLAG_INFO; int ncol = table.getColumnCount(); final ColumnInfo[] colInfos = new ColumnInfo[ ncol ]; int modified = 0; for ( int icol = 0; icol < ncol; icol++ ) { ColumnInfo cinfo = new ColumnInfo( table.getColumnInfo( icol ) ); boolean isUbyte = Boolean.TRUE .equals( cinfo.getAuxDatumValue( ubyteKey, Boolean.class ) ); Class clazz = cinfo.getContentClass(); if ( magicNulls && cinfo.isNullable() && Number.class.isAssignableFrom( clazz ) && cinfo.getAuxDatum( badKey ) == null ) { Number badValue; if ( isUbyte ) { badValue = new Short( (short) 0xff ); } else if ( clazz == Byte.class || clazz == Short.class ) { badValue = new Short( Short.MIN_VALUE ); } else if ( clazz == Integer.class ) { badValue = new Integer( Integer.MIN_VALUE ); } else if ( clazz == Long.class ) { badValue = new Long( Long.MIN_VALUE ); } else { badValue = null; } if ( badValue != null ) { modified++; cinfo.getAuxData() .add( new DescribedValue( badKey, badValue ) ); } } if ( ! magicNulls && ! cinfo.isArray() ) { DescribedValue nv = cinfo.getAuxDatum( badKey ); if ( nv != null ) { cinfo.getAuxData().remove( nv ); modified++; } } if ( ! allowXtype ) { String xt = cinfo.getXtype(); if ( xt != null ) { cinfo.setXtype( null ); modified++; } } colInfos[ icol ] = cinfo; } if ( modified > 0 ) { table = new WrapperStarTable( table ) { public ColumnInfo getColumnInfo( int icol ) { return colInfos[ icol ]; } }; } return table; } /** * Returns a serializer capable of serializing a given table to * given data format, using the default VOTable output version. * * @param dataFormat one of the supported VOTable serialization formats * @param table the table to be serialized * @return serializer */ public static VOSerializer makeSerializer( DataFormat dataFormat, StarTable table ) throws IOException { return makeSerializer( dataFormat, VOTableVersion.getDefaultVersion(), table ); } /** * Returns a serializer capable of serializing * a given table to a given data format using a given VOTable version. * * @param dataFormat one of the supported VOTable serialization formats * @param version specifies the version of the VOTable standard * to which the output will conform * @param table the table to be serialized * @return serializer */ public static VOSerializer makeSerializer( DataFormat dataFormat, VOTableVersion version, StarTable table ) throws IOException { /* Prepare. */ boolean magicNulls = ( dataFormat == DataFormat.BINARY ) || ( dataFormat == DataFormat.FITS ) || ( dataFormat == DataFormat.TABLEDATA && ! version.allowEmptyTd() ); table = prepareForSerializer( table, magicNulls, version.allowXtype() ); /* Return a serializer. */ if ( dataFormat == DataFormat.TABLEDATA ) { TabledataVOSerializer tdser = new TabledataVOSerializer( table, version, magicNulls ); tdser.setCompact( table.getColumnCount() <= 4 ); return tdser; } else if ( dataFormat == DataFormat.FITS ) { /* Use some fairly innocuous configuration here. * It would be possible to provide user-level configuration * options, but the FITS serialization format is very little used, * so don't bother unless some compelling use case arises. */ FitsTableSerializerConfig config = new FitsTableSerializerConfig() { public boolean allowSignedByte() { return false; } public WideFits getWide() { return null; } public boolean allowZeroLengthString() { return false; } public byte getPadCharacter() { return (byte) '\0'; } }; return new FITSVOSerializer( table, version, new StandardFitsTableSerializer( config, table ) ); } else if ( dataFormat == DataFormat.BINARY ) { return new BinaryVOSerializer( table, version, magicNulls ); } else if ( dataFormat == DataFormat.BINARY2 ) { if ( version.allowBinary2() ) { return new Binary2VOSerializer( table, version, magicNulls ); } else { throw new IllegalArgumentException( "BINARY2 format not legal " + "for VOTable " + version ); } } else { throw new AssertionError( "No such format " + dataFormat.toString() ); } } /** * Constructs a FITS-type VOSerializer. Since a FitsTableSerializer is * required for this, if one is already available then supplying it * directly here will be more efficient than calling * makeSerializer which will have to construct another, * possibly an expensive step. * * @param table table for serialization * @param fitser fits serializer * @param version output VOTable version * @return serializer */ public static VOSerializer makeFitsSerializer( StarTable table, FitsTableSerializer fitser, VOTableVersion version ) throws IOException { table = prepareForSerializer( table, false, true ); return new FITSVOSerializer( table, version, fitser ); } // // A couple of non-public static methods follow which are used by // both the TABLEDATA and the BINARY serializers. These are only // in this class because they have to be somewhere - they should // really be methods of an abstract superclass of the both of them, // but this is impossible since the BINARY one already inherits // from StreamableVOSerializer. Multiple inheritance would be // nice for once. // /** * Returns the set of encoders used to encode a given StarTable in * one of the native formats (BINARY or TABLEDATA). * * @param table the table to characterise * @return an array of encoders used for encoding its data */ private static Encoder[] getEncoders( StarTable table, boolean magicNulls ) { int ncol = table.getColumnCount(); Encoder[] encoders = new Encoder[ ncol ]; for ( int icol = 0; icol < ncol; icol++ ) { ColumnInfo info = table.getColumnInfo( icol ); boolean isUnicode = "unicodeChar" .equals( info.getAuxDatumValue( VOStarTable.DATATYPE_INFO, String.class ) ); encoders[ icol ] = Encoder.getEncoder( info, magicNulls, isUnicode ); if ( encoders[ icol ] == null ) { logger.warning( "Can't serialize column " + info + " of type " + info.getContentClass().getName() ); } } return encoders; } /** * Writes the FIELD elements corresponding to a set of Encoders. * * @param encoders the list of encoders (some may be null) * @param table the table being serialized * @param coosysMap MetaEl->ID map for COOSYS elements * that will be available * @param timesysMap MetaEl->ID map for TIMESYS elements * that will be available * @param writer destination stream */ private static void outputFields( Encoder[] encoders, StarTable table, Map coosysMap, Map timesysMap, BufferedWriter writer ) throws IOException { int ncol = encoders.length; for ( int icol = 0; icol < ncol; icol++ ) { Encoder encoder = encoders[ icol ]; if ( encoder != null ) { String content = encoder.getFieldContent(); Map atts = getFieldAttributes( encoder, coosysMap, timesysMap ); writeFieldElement( writer, content, atts ); } else { writer.write( "" ); writer.newLine(); } } } /** * TABLEDATA implementation of VOSerializer. */ private static class TabledataVOSerializer extends VOSerializer { private final Encoder[] encoders; TabledataVOSerializer( StarTable table, VOTableVersion version, boolean magicNulls ) { super( table, DataFormat.TABLEDATA, version ); encoders = getEncoders( table, magicNulls ); } public void writeFields( BufferedWriter writer ) throws IOException { outputFields( encoders, getTable(), coosysMap_, timesysMap_, writer ); } public void writeInlineDataElement( BufferedWriter writer ) throws IOException { writer.write( "" ); writer.newLine(); writer.write( "" ); writer.newLine(); TdTags tdTags = new TdTags( isCompact() ); int ncol = encoders.length; RowSequence rseq = getTable().getRowSequence(); try { while ( rseq.next() ) { writer.write( tdTags.preTr_ ); Object[] rowdata = rseq.getRow(); for ( int icol = 0; icol < ncol; icol++ ) { Encoder encoder = encoders[ icol ]; if ( encoder != null ) { String text = encoder.encodeAsText( rowdata[ icol ] ); writer.write( tdTags.preTd_ ); writer.write( formatText( text ) ); writer.write( tdTags.postTd_ ); } } writer.write( tdTags.postTr_ ); } } finally { rseq.close(); } writer.write( "" ); writer.newLine(); writer.write( "" ); writer.newLine(); writer.flush(); } public void writeInlineDataElementUTF8( OutputStream out ) throws IOException { DataBufferedOutputStream dout = new DataBufferedOutputStream( out ); TdTags tdTags = new TdTags( isCompact() ); byte[] preTr = toAsciiBytes( tdTags.preTr_ ); byte[] preTd = toAsciiBytes( tdTags.preTd_ ); byte[] postTd = toAsciiBytes( tdTags.postTd_ ); byte[] postTr = toAsciiBytes( tdTags.postTr_ ); dout.writeBytes( "" ); dout.write( NL_BYTES ); dout.writeBytes( "" ); dout.write( NL_BYTES ); int ncol = encoders.length; try ( RowSequence rseq = getTable().getRowSequence() ) { while ( rseq.next() ) { dout.write( preTr ); Object[] rowdata = rseq.getRow(); for ( int icol = 0; icol < ncol; icol++ ) { Encoder encoder = encoders[ icol ]; if ( encoder != null ) { String text = encoder.encodeAsText( rowdata[ icol ] ); dout.write( preTd ); writeEscapedTextUTF8( text, dout ); dout.write( postTd ); } } dout.write( postTr ); } } dout.writeBytes( "" ); dout.write( NL_BYTES ); dout.writeBytes( "" ); dout.write( NL_BYTES ); dout.flush(); } public void writeHrefDataElement( BufferedWriter writer, String href, OutputStream streamout ) { throw new UnsupportedOperationException( "TABLEDATA only supports inline output" ); } /** * Provides tag representations for TD and TR elements. */ private static class TdTags { final String preTr_; final String preTd_; final String postTd_; final String postTr_; /** * Constructor. * * @param isCompact true for compact output, false for whitespace padding */ TdTags( boolean isCompact ) { if ( isCompact ) { preTr_ = ""; preTd_ = ""; postTd_ = ""; postTr_ = "" + NL_STRING; } else { preTr_ = " " + NL_STRING; preTd_ = " "; postTd_ = "" + NL_STRING; postTr_ = " " + NL_STRING; } } } } /** * Abstract subclass for VOSerializers which write their data as * binary output (bytes rather than characters) to a STREAM element. * This class is package-private (rather than private) since it is * used by VOTableWriter for efficiency reasons. */ static abstract class StreamableVOSerializer extends VOSerializer { private final String tagname; /** * Initialises this serializer. * * @param table the table it will serialize * @param format serialization format * @param tagname the name of the XML element that contains the data */ private StreamableVOSerializer( StarTable table, DataFormat format, VOTableVersion version, String tagname ) { super( table, format, version ); this.tagname = tagname; } /** * Writes raw binary data representing the table data cells * to an output stream. These are the data which are contained in the * STREAM element of a VOTable document. * No markup (e.g. the STREAM start/end tags) should be included. * * @param out destination stream */ public abstract void streamData( OutputStream out ) throws IOException; public void writeInlineDataElement( BufferedWriter writer ) throws IOException { /* Start the DATA element. */ writer.write( "" ); writer.newLine(); writer.write( "<" + tagname + ">" ); writer.newLine(); /* Write the STREAM element. */ writer.write( "" ); writer.newLine(); /* Note this implementation is considerably faster than the Java 8 * java.util.Base64 implementation; it's also more suitable because * the Base64 output can be terminated without closing the * underlying output stream (or jumping through hoops to avoid * doing so). */ BufferedBase64OutputStream b64out = new BufferedBase64OutputStream( new WriterOutputStream( writer ) ); streamData( b64out ); b64out.endBase64(); writer.write( "" ); writer.newLine(); /* Finish off the DATA element. */ writer.write( "" ); writer.newLine(); writer.write( "" ); writer.newLine(); } public void writeInlineDataElementUTF8( OutputStream out ) throws IOException { out.write( toAsciiBytes( "" ) ); out.write( NL_BYTES ); out.write( toAsciiBytes( "<" + tagname + ">" ) ); out.write( NL_BYTES ); out.write( toAsciiBytes( "" ) ); out.write( NL_BYTES ); BufferedBase64OutputStream b64out = new BufferedBase64OutputStream( out ); streamData( b64out ); b64out.endBase64(); out.write( toAsciiBytes( "" ) ); out.write( NL_BYTES ); out.write( toAsciiBytes( "" ) ); out.write( NL_BYTES ); out.write( toAsciiBytes( "" ) ); out.write( NL_BYTES ); } public void writeHrefDataElement( BufferedWriter xmlwriter, String href, OutputStream streamout ) throws IOException { /* Start the DATA element. */ xmlwriter.write( "" ); xmlwriter.newLine(); xmlwriter.write( '<' + tagname + '>' ); xmlwriter.newLine(); /* Write the STREAM element. */ xmlwriter.write( "" ); xmlwriter.newLine(); /* Finish the DATA element. */ xmlwriter.write( "" ); xmlwriter.newLine(); xmlwriter.write( "" ); xmlwriter.newLine(); /* Write the bulk data to the output stream. */ streamData( streamout ); } } /** * BINARY format implementation of VOSerializer. */ private static class BinaryVOSerializer extends StreamableVOSerializer { private final Encoder[] encoders; BinaryVOSerializer( StarTable table, VOTableVersion version, boolean magicNulls ) { super( table, DataFormat.BINARY, version, "BINARY" ); encoders = getEncoders( table, magicNulls ); } public void writeFields( BufferedWriter writer ) throws IOException { outputFields( encoders, getTable(), coosysMap_, timesysMap_, writer ); } public void streamData( OutputStream out ) throws IOException { int ncol = encoders.length; DataBufferedOutputStream dout = new DataBufferedOutputStream( out ); try ( RowSequence rseq = getTable().getRowSequence() ) { while ( rseq.next() ) { Object[] row = rseq.getRow(); for ( int icol = 0; icol < ncol; icol++ ) { Encoder encoder = encoders[ icol ]; if ( encoder != null ) { encoder.encodeToStream( row[ icol ], dout ); } } } } dout.flush(); } } /** * BINARY2 format implementation of VOSerializer. */ private static class Binary2VOSerializer extends StreamableVOSerializer { private final Encoder[] encoders; Binary2VOSerializer( StarTable table, VOTableVersion version, boolean magicNulls ) { super( table, DataFormat.BINARY2, version, "BINARY2" ); encoders = getEncoders( table, magicNulls ); } public void writeFields( BufferedWriter writer ) throws IOException { outputFields( encoders, getTable(), coosysMap_, timesysMap_, writer ); } public void streamData( OutputStream out ) throws IOException { /* Restrict attention to columns with non-null encoders, * that is those which we will actually be writing out. */ IntList icolList = new IntList( encoders.length ); for ( int icol = 0; icol < encoders.length; icol++ ) { if ( encoders[ icol ] != null ) { icolList.add( icol ); } } int[] icols = icolList.toIntArray(); int ncol = icols.length; boolean[] nullFlags = new boolean[ ncol ]; /* Read data from table. */ DataBufferedOutputStream dout = new DataBufferedOutputStream( out ); try ( RowSequence rseq = getTable().getRowSequence() ) { while ( rseq.next() ) { /* Prepare and write the null-flag array. */ Object[] row = rseq.getRow(); for ( int jcol = 0; jcol < ncol; jcol++ ) { int icol = icols[ jcol ]; Object cell = row[ icol ]; nullFlags[ jcol ] = cell == null; } FlagIO.writeFlags( dout, nullFlags ); /* Write the data cells. */ for ( int jcol = 0; jcol < ncol; jcol++ ) { int icol = icols[ jcol ]; Object cell = row[ icol ]; encoders[ icol ].encodeToStream( cell, dout ); } } } dout.flush(); } } /** * FITS format implementation of VOSerializer. */ private static class FITSVOSerializer extends StreamableVOSerializer { private final FitsTableSerializer fitser; FITSVOSerializer( StarTable table, VOTableVersion version, FitsTableSerializer fitser ) throws IOException { super( table, DataFormat.FITS, version, "FITS" ); this.fitser = fitser; } public void writeFields( BufferedWriter writer ) throws IOException { int ncol = getTable().getColumnCount(); for ( int icol = 0; icol < ncol; icol++ ) { /* Get information about how this column is going to be * written by the FITS serializer. */ char tform = fitser.getFormatChar( icol ); int[] dims = fitser.getDimensions( icol ); String badval = fitser.getBadValue( icol ); /* Only write a FIELD element if the FITS serializer is going * to serialize it. */ if ( dims != null ) { /* Get the basic information for this column. */ Encoder encoder = Encoder.getEncoder( getTable().getColumnInfo( icol ), true, false ); String content = encoder.getFieldContent(); Map atts = getFieldAttributes( encoder, coosysMap_, timesysMap_ ); /* Modify the datatype attribute to match what the FITS * serializer will write. */ String datatype; switch ( tform ) { case 'L': datatype = "boolean"; break; case 'X': datatype = "bit"; break; case 'B': datatype = "unsignedByte"; break; case 'I': datatype = "short"; break; case 'J': datatype = "int"; break; case 'K': datatype = "long"; break; case 'A': datatype = "char"; break; case 'E': datatype = "float"; break; case 'D': datatype = "double"; break; case 'C': datatype = "floatComplex"; break; case 'M': datatype = "doubleComplex"; break; default: throw new AssertionError( "Unknown format letter " + tform ); } atts.put( "datatype", datatype ); /* Modify the arraysize attribute to match what the FITS * serializer will write. */ if ( dims.length == 0 ) { if ( ! "1".equals( atts.get( "arraysize" ) ) ) { atts.remove( "arraysize" ); } } else { StringBuffer arraysize = new StringBuffer(); for ( int i = 0; i < dims.length; i++ ) { if ( i > 0 ) { arraysize.append( 'x' ); } arraysize.append( dims[ i ] ); } atts.put( "arraysize", arraysize.toString() ); } /* Modify the VALUES text to match what the FITS serializer * will write. */ encoder.setNullString( badval ); /* Write out the FIELD element with attributes which match * the way the FITS serializer will write the table. */ writeFieldElement( writer, content, atts ); } else { writer.write( "" ); writer.newLine(); } } } public void streamData( OutputStream out ) throws IOException { FitsUtil.writeEmptyPrimary( out ); new FitsTableWriter().writeTableHDU( getTable(), fitser, out ); } } /** * Adapter class which turns a Writer into an OutputStream. * This is used for writing base64 down - * we don't worrry about encodings here since the only characters * going down the writer will be base64-type characters, which * can just be typecast to bytes. */ private static class WriterOutputStream extends OutputStream { Writer writer; static final int BUFLENG = 10240; char[] mainBuf = new char[ BUFLENG ]; WriterOutputStream( Writer writer ) { this.writer = writer; } public void close() throws IOException { writer.close(); } public void flush() throws IOException { writer.flush(); } public void write( byte[] b ) throws IOException { write( b, 0, b.length ); } public void write( byte[] b, int off, int len ) throws IOException { char[] buf = len <= BUFLENG ? mainBuf : new char[ len ]; for ( int i = 0; i < len; i++ ) { buf[ i ] = (char) b[ off++ ]; } writer.write( buf, 0, len ); } public void write(int b) throws IOException { writer.write( b ); } } /** * Returns the attributes required for a FIELD/PARAM element given the * Encoder being used to write the column or parameter in question. * * @param encoder encoder to write FIELD/PARAM data * @param coosysMap MetaEl->ID map for COOSYS elements that will be * available in the output document * @param timesysMap MetaEl->ID map for TIMESYS elements that will be * available in the output document * @return map of FIELD/PARAM attribute name->value pairs */ private static Map getFieldAttributes( Encoder encoder, Map coosysMap, Map timesysMap ) { /* Query encoder for basic items. */ Map map = encoder.getFieldAttributes(); /* Add a ref attribute pointing to a COOSYS or TIMESYS element if one * that matches the requirements of the element in question * has been provided. Note this relies on the fact that * the MetaEl class has suitable equality semantics. */ ValueInfo info = encoder.getInfo(); MetaEl coosys = getCoosys( info ); MetaEl timesys = getTimesys( info ); String csId = coosysMap != null ? coosysMap.get( coosys ) : null; String tsId = timesysMap != null ? timesysMap.get( timesys ) : null; if ( csId != null ) { map.put( "ref", csId ); } else if ( tsId != null ) { map.put( "ref", tsId ); } return map; } /** * Returns the MetaEl object corresponding to the COOSYS metadata * for a given ValueInfo, if such metadata is present. * * @param info item metadata * @retun MetaEl object representing COOSYS, or null if none required */ private static MetaEl getCoosys( ValueInfo info ) { Map map = new LinkedHashMap(); addAtt( map, info, VOStarTable.COOSYS_SYSTEM_INFO, "system" ); addAtt( map, info, VOStarTable.COOSYS_EPOCH_INFO, "epoch" ); addAtt( map, info, VOStarTable.COOSYS_EQUINOX_INFO, "equinox" ); return map.size() > 0 ? new MetaEl( "COOSYS", map ) : null; } /** * Returns the MetaEl object corresponding to the TIMESYS metadata * for a given ValueInfo, if such metadata is present. * * @param info item metadata * @retun MetaEl object representing TIMESYS, or null if none required */ private static MetaEl getTimesys( ValueInfo info ) { Map map = new LinkedHashMap(); addAtt( map, info, VOStarTable.TIMESYS_TIMEORIGIN_INFO, "timeorigin" ); addAtt( map, info, VOStarTable.TIMESYS_TIMESCALE_INFO, "timescale" ); addAtt( map, info, VOStarTable.TIMESYS_REFPOSITION_INFO, "refposition"); return map.size() > 0 ? new MetaEl( "TIMESYS", map ) : null; } /** * Utility method to add an item to the MetaEl attribute map * given value metadata. * * @param map attribute map to augment * @param info info item containing aux metadata * @param auxKey aux metadata key for a String item * @param attname name of entry in attribute map */ private static void addAtt( Map map, ValueInfo info, ValueInfo auxKey, String attname ) { DescribedValue dval = info.getAuxDatumByName( auxKey.getName() ); if ( dval != null ) { String value = dval.getTypedValue( String.class ); if ( value != null && value.trim().length() > 0 ) { map.put( attname, value ); } } } /** * Represents an element with a given name and set of relevant attributes. * The implementation is just a very thin wrapper round a * name->value attribute map. * The equals and hashMap methods are implemented such that * instances with the same element name and attribute values * will evaluate as equal. */ private static class MetaEl { private final String elName_; private final Map attMap_; /** * Constructor. * * @param elName element name * @param attMap attribute name->value map, * excluding the ID attribute */ MetaEl( String elName, Map attMap ) { elName_ = elName; attMap_ = Collections.unmodifiableMap( attMap ); } /** * Returns the XML serialization of this object as an element. * * @param id value of ID attribute * @return element XML serialization */ public String toXml( String id ) { return new StringBuffer() .append( "<" ) .append( elName_ ) .append( formatAttribute( "ID", id ) ) .append( formatAttributes( attMap_ ) ) .append( "/>" ) .toString(); } @Override public int hashCode() { int code = 442041; code = 23 * code + elName_.hashCode(); code = 23 * code + attMap_.hashCode(); return code; } @Override public boolean equals( Object o ) { if ( o instanceof MetaEl ) { MetaEl other = (MetaEl) o; return this.elName_.equals( other.elName_ ) && this.attMap_.equals( other.attMap_ ); } else { return false; } } } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy