From c43513e0dd1d34b90816ac660eb40130fad464b4 Mon Sep 17 00:00:00 2001 From: Stelios Voutsinas <steliosvoutsinas@yahoo.com> Date: Tue, 7 Jan 2025 18:00:36 -0700 Subject: [PATCH 1/3] Add ResultSetWriter and enable for B2 VOTable --- build.gradle | 6 +- ...250114_162646_steliosvoutsinas_DM_48132.md | 13 + .../tap/impl/MaxRecValidatorImpl.java | 1 - .../opencadc/tap/impl/QServQueryRunner.java | 4 +- .../opencadc/tap/impl/ResultSetWriter.java | 717 ++++++++++++++++++ .../opencadc/tap/impl/RubinFormatFactory.java | 2 +- .../opencadc/tap/impl/RubinTableWriter.java | 218 ++++-- 7 files changed, 911 insertions(+), 50 deletions(-) create mode 100644 changelog.d/20250114_162646_steliosvoutsinas_DM_48132.md create mode 100644 src/main/java/org/opencadc/tap/impl/ResultSetWriter.java diff --git a/build.gradle b/build.gradle index da75478..0304cec 100644 --- a/build.gradle +++ b/build.gradle @@ -36,7 +36,7 @@ war { configurations { intTestCompile.extendsFrom testImplementation intTestRuntime.extendsFrom testRuntimeOnly - implementation.exclude group: 'uk.ac.starlink' + //implementation.exclude group: 'uk.ac.starlink' } dependencies { @@ -56,6 +56,10 @@ dependencies { implementation 'org.opencadc:cadc-uws:1.0.5' implementation 'org.opencadc:cadc-uws-server:1.2.21' implementation 'org.opencadc:cadc-vosi:1.4.6' + implementation 'uk.ac.starlink:stil:[4.0,5.0)' + + implementation 'uk.ac.starlink:jcdf:[1.2.3,2.0)' + implementation 'uk.ac.starlink:stil:[4.0,5.0)' // Switch out this to use any supported database instead of PostgreSQL. // ## START CUSTOM DATABASE ## diff --git a/changelog.d/20250114_162646_steliosvoutsinas_DM_48132.md b/changelog.d/20250114_162646_steliosvoutsinas_DM_48132.md new file mode 100644 index 0000000..d0559b5 --- /dev/null +++ b/changelog.d/20250114_162646_steliosvoutsinas_DM_48132.md @@ -0,0 +1,13 @@ +<!-- Delete the sections that don't apply --> + +### Added + +- ResultSetWriter class, used to write out Binary2 VOTable serialization + +### Changed + +- Added new Binary2 VOTable serialization, make default. Retain tabledata as an optional format + +### Fixed + +- Correctly print field metadata diff --git a/src/main/java/org/opencadc/tap/impl/MaxRecValidatorImpl.java b/src/main/java/org/opencadc/tap/impl/MaxRecValidatorImpl.java index 6380f91..e4ab9a8 100644 --- a/src/main/java/org/opencadc/tap/impl/MaxRecValidatorImpl.java +++ b/src/main/java/org/opencadc/tap/impl/MaxRecValidatorImpl.java @@ -93,7 +93,6 @@ public MaxRecValidatorImpl() { @Override public Integer validate() { - LOGGER.info(""); if (super.sync) { try { // no limits on sync diff --git a/src/main/java/org/opencadc/tap/impl/QServQueryRunner.java b/src/main/java/org/opencadc/tap/impl/QServQueryRunner.java index 4140d4b..f472508 100644 --- a/src/main/java/org/opencadc/tap/impl/QServQueryRunner.java +++ b/src/main/java/org/opencadc/tap/impl/QServQueryRunner.java @@ -371,7 +371,7 @@ private void doIt() // and restrict to forward only so that client memory usage is minimal since // we are only interested in reading the ResultSet once - connection.setAutoCommit(false); + connection.setAutoCommit(false); pstmt = connection.prepareStatement(sql); pstmt.setFetchSize(1000); pstmt.setFetchDirection(ResultSet.FETCH_FORWARD); @@ -397,7 +397,7 @@ private void doIt() tableWriter.write(resultSet, syncOutput.getOutputStream()); else tableWriter.write(resultSet, syncOutput.getOutputStream(), maxRows.longValue()); - + t2 = System.currentTimeMillis(); dt = t2 - t1; t1 = t2; diagnostics.add(new Result("diag", URI.create("query:stream:"+dt))); } diff --git a/src/main/java/org/opencadc/tap/impl/ResultSetWriter.java b/src/main/java/org/opencadc/tap/impl/ResultSetWriter.java new file mode 100644 index 0000000..b4ab6c5 --- /dev/null +++ b/src/main/java/org/opencadc/tap/impl/ResultSetWriter.java @@ -0,0 +1,717 @@ +package org.opencadc.tap.impl; + +import ca.nrc.cadc.dali.tables.TableWriter; +import ca.nrc.cadc.dali.tables.votable.GroupElement; +import ca.nrc.cadc.dali.tables.votable.ParamElement; +import ca.nrc.cadc.dali.tables.votable.VOTableField; +import ca.nrc.cadc.dali.tables.votable.VOTableGroup; +import ca.nrc.cadc.dali.tables.votable.VOTableInfo; +import ca.nrc.cadc.dali.tables.votable.VOTableParam; +import ca.nrc.cadc.dali.tables.votable.VOTableResource; +import ca.nrc.cadc.dali.util.Format; +import ca.nrc.cadc.dali.util.FormatFactory; +import ca.nrc.cadc.xml.ContentConverter; +import ca.nrc.cadc.xml.IterableContent; +import ca.nrc.cadc.xml.MaxIterations; +import uk.ac.starlink.table.ColumnInfo; +import uk.ac.starlink.table.DescribedValue; +import uk.ac.starlink.table.RowSequence; +import uk.ac.starlink.table.WrapperRowSequence; +import uk.ac.starlink.table.jdbc.SequentialResultSetStarTable; +import uk.ac.starlink.votable.DataFormat; +import uk.ac.starlink.votable.VOSerializer; +import uk.ac.starlink.votable.VOTableVersion; + +import java.io.BufferedWriter; +import java.io.IOException; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.Writer; +import java.net.MalformedURLException; +import java.net.URL; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +import org.apache.log4j.Logger; +import org.jdom2.Document; +import org.jdom2.Element; +import org.jdom2.Namespace; +import org.jdom2.output.XMLOutputter; + +/** + * ResultSet to VOTable writer. + * + * @author stvoutsin + */ +public class ResultSetWriter implements TableWriter<ResultSet> { + + private static final Logger log = Logger.getLogger(ResultSetWriter.class); + private static final String BASE_URL = System.getProperty("base_url"); + + public static final String CONTENT_TYPE = "application/x-votable+xml"; + public static final String CONTENT_TYPE_ALT = "text/xml"; + + // VOTable Version number. + public static final String VOTABLE_VERSION = "1.4"; + + // Uri to the XML schema. + public static final String XSI_SCHEMA = "http://www.w3.org/2001/XMLSchema-instance"; + + // Uri to the VOTable schema. + public static final String VOTABLE_11_NS_URI = "http://www.ivoa.net/xml/VOTable/v1.1"; + public static final String VOTABLE_12_NS_URI = "http://www.ivoa.net/xml/VOTable/v1.2"; + public static final String VOTABLE_13_NS_URI = "http://www.ivoa.net/xml/VOTable/v1.3"; + public static final String VOTABLE_14_NS_URI = "http://www.ivoa.net/xml/VOTable/v1.4"; + + private FormatFactory formatFactory; + private String mimeType; + private DataFormat dfmt_; + private VOTableVersion version_; + private long maxrec_; + private List<VOTableInfo> infos; + private List<VOTableResource> resources; + private ColumnInfo[] columns; + private long totalRows = 0; + + /** + * Get the total number of rows processed. + * + * @return the number of rows written + */ + public long getRowCount() { + return totalRows; + } + + /** + * Default constructor. + */ + public ResultSetWriter() { + this(null, uk.ac.starlink.votable.DataFormat.BINARY2, VOTableVersion.V14, -1); + } + + + /** + * Constructor. + * + * @param mimeType selects the mimetype string + * @param dfmt selects VOTable serialization format + * (TABLEDATA, BINARY, BINARY2, FITS) + * @param version selects VOTable version + * @param maxrec maximum record count before overflow; + * negative value means no limit + */ + public ResultSetWriter( String mimeType, DataFormat dfmt, VOTableVersion version, long maxrec ) { + this.mimeType = mimeType; + this.dfmt_ = dfmt; + this.version_ = version; + this.maxrec_ = maxrec; + this.infos = new ArrayList<>(); + this.resources = new ArrayList<>(); + this.columns = null; + } + + + /** + * Get the Content-Type for the VOTable. + * + * @return VOTable Content-Type. + */ + @Override + public String getContentType() { + if (mimeType == null) { + return CONTENT_TYPE; + } + + return mimeType; + } + + public List<VOTableResource> getResources() { + return resources; + } + + /** + * Get the list of VOTable infos. + * + * @return List of VOTableInfo objects + */ + public List<VOTableInfo> getInfos() { + return infos; + } + + public ColumnInfo[] getColumns() { + return columns; + } + + /** + * Set the list of VOTable infos. + * + * @param infos List of VOTableInfo objects + */ + public void setInfos(List<VOTableInfo> infos) { + this.infos = infos; + } + + public void setColumns(ColumnInfo[] columns) { + this.columns = columns; + } + + + /** + * Set the resources of the VOTable. + * + * @param resources List of VOTableResource objects + */ + public void setResources(List<VOTableResource> resources) { + this.resources = resources; + } + + /** + * Add a single VOTable info. + * + * @param info VOTableInfo object to add + */ + public void addInfo(VOTableInfo info) { + if (info != null) { + this.infos.add(info); + } + } + + + /** + * Add a single VOTableResource object. + * + * @param resource VOTableResource object to add + */ + public void addResource(VOTableResource resource) { + if (resource != null) { + this.resources.add(resource); + } + } + /** + * Get error content type + */ + public String getErrorContentType() { + return getContentType(); + } + + /** + * Get the extension for the VOTable. + * + * @return VOTable extension. + */ + @Override + public String getExtension() { + return "xml"; + } + + @Override + public void setFormatFactory(FormatFactory formatFactory) { + this.formatFactory = formatFactory; + } + + /** + * Write the ResultSet to the specified OutputStream. + * + * @param resultSet ResultSet object to write. + * @param ostream OutputStream to write to. + * @throws IOException if problem writing to OutputStream. + */ + @Override + public void write(ResultSet resultSet, OutputStream ostream) + throws IOException { + write(resultSet, ostream, Long.MAX_VALUE); + } + + /** + * Write the ResultSet to the specified OutputStream, only writing maxrec rows. + * If the VOTable contains more than maxrec rows, appends an INFO element with + * name="QUERY_STATUS" value="OVERFLOW" to the VOTable. + * + * @param resultSet ResultSet object to write. + * @param ostream OutputStream to write to. + * @param maxrec maximum number of rows to write. + * @throws IOException if problem writing to OutputStream. + */ + @Override + public void write(ResultSet resultSet, OutputStream ostream, Long maxrec) + throws IOException { + Writer writer = new BufferedWriter(new OutputStreamWriter(ostream, "UTF-8")); + write(resultSet, writer, maxrec); + } + + /** + * Write the ResultSet to the specified Writer. + * + * @param resultSet ResultSet object to write. + * @param writer Writer to write to. + * @throws IOException if problem writing to the writer. + */ + @Override + public void write(ResultSet resultSet, Writer writer) + throws IOException { + write(resultSet, writer, Long.MAX_VALUE); + } + + /** + * Write the ResultSet to the specified Writer, only writing maxrec rows. + * If the ResultSet contains more than maxrec rows, appends an INFO element with + * name="QUERY_STATUS" value="OVERFLOW" to the VOTable. + * + * @param resultSet ResultSet object to write. + * @param writer Writer to write to. + * @param maxrec maximum number of rows to write. + * @throws IOException if problem writing to the writer. + */ + @Override + public void write(ResultSet resultSet, Writer writer, Long maxrec) + throws IOException { + if (formatFactory == null) { + this.formatFactory = new FormatFactory(); + } + writeImpl(resultSet, writer, maxrec); + } + + /** + * Write the Throwable to a VOTable, creating an INFO element with + * name="QUERY_STATUS" value="ERROR" and setting the stacktrace as + * the INFO text. + * + * @param thrown Throwable to write. + * @param output OutputStream to write to. + * @throws IOException if problem writing to the stream. + */ + public void write(Throwable thrown, OutputStream output) + throws IOException { + Document document = createDocument(); + Element root = document.getRootElement(); + Namespace namespace = root.getNamespace(); + + // Create the RESOURCE element and add to the VOTABLE element. + Element resource = new Element("RESOURCE", namespace); + resource.setAttribute("type", "results"); + root.addContent(resource); + + // Create the INFO element and add to the RESOURCE element. + Element info = new Element("INFO", namespace); + info.setAttribute("name", "QUERY_STATUS"); + info.setAttribute("value", "ERROR"); + info.setText(getThrownExceptions(thrown)); + resource.addContent(info); + + // Write out the VOTABLE. + XMLOutputter outputter = new XMLOutputter(); + outputter.setFormat(org.jdom2.output.Format.getPrettyFormat()); + outputter.output(document, output); + } + + /** + * Writes a result set to an output stream as a VOTable. + * + * @param resultSet ResultSet + * @param writer Writer + * @param maxrec Maximum number of rows + */ + protected void writeImpl(ResultSet resultSet, Writer writer, Long maxrec) + throws IOException { + log.debug("write, maxrec=" + maxrec); + + + try (BufferedWriter out = (writer instanceof BufferedWriter) + ? (BufferedWriter) writer + : new BufferedWriter(writer)) { + + LimitedResultSetStarTable table; + try { + table = new LimitedResultSetStarTable(this.getColumns(), resultSet, maxrec); + } catch (SQLException e) { + throw new IOException("Error processing ResultSet: " + e.getMessage(), e); + } + + /* Prepares the object that will do the serialization work. */ + VOSerializer voser = + VOSerializer.makeSerializer( dfmt_, version_, table ); + + /* Write header. */ + out.write( "<VOTABLE" + + VOSerializer.formatAttribute( "version", + version_.getVersionNumber() ) + + VOSerializer.formatAttribute( "xmlns", + version_.getXmlNamespace() ) + + ">" ); + out.newLine(); + out.write( "<RESOURCE>" ); + out.newLine(); + + XMLOutputter outputter = new XMLOutputter(); + outputter.setFormat(org.jdom2.output.Format.getPrettyFormat()); + + // Write all info elements + for (VOTableInfo info : infos) { + Element infoElement = new Element("INFO"); + infoElement.setAttribute("name", info.getName()); + infoElement.setAttribute("value", info.getValue()); + outputter.output(infoElement, out); + out.newLine(); + } + + /* Write table element. */ + voser.writeInlineTableElement( out ); + + /* Check for overflow and write INFO if required. */ + if ( table.lastSequenceOverflowed() ) { + out.write( "<INFO name='QUERY_STATUS' value='OVERFLOW'/>" ); + out.newLine(); + } + + /* Write footer. */ + out.write( "</RESOURCE>" ); + out.newLine(); + for (VOTableResource resource : resources) { + Element r = createResource(resource, null); + outputter.output(r, out); + out.newLine(); + } + out.write( "</VOTABLE>" ); + out.newLine(); + out.flush(); + + this.totalRows = table.getTotalRows(); + + } + } + + /** + * StarTable implementation which is based on a ResultSet, and which + * is limited to a fixed number of rows when its row iterator is used. + * Note this implementation is OK for one-pass table output handlers + * like VOTable, but won't work for ones which require two passes + * such as FITS (which needs row count up front). + */ + private static class LimitedResultSetStarTable + extends SequentialResultSetStarTable { + + public static final String TABLE_NAME_INFO = "TABLE_NAME"; + private final long maxrec_; + private boolean overflow_; + private long totalRows = 0; + private ColumnInfo[] columnInfos; + + /** + * Constructor. + * + * @param rset result set supplying the data + * @param maxrec maximum number of rows that will be iterated over; + * negative value means no limit + */ + LimitedResultSetStarTable(ColumnInfo[] colInfos, ResultSet rset, long maxrec ) + throws SQLException { + super( rset ); + maxrec_ = maxrec; + columnInfos = colInfos; + } + + /** + * Get the row count + */ + public long getTotalRows() { + return totalRows; + } + + /** + * Provides column information for a specified index. + * + * @param colInd index of the column to describe + * @return metadata object describing this column + */ + @Override + public ColumnInfo getColumnInfo(final int colInd) { + return columnInfos[colInd]; + } + + /** + * Indicates whether the last row sequence dispensed by + * this table's getRowSequence method was truncated at maxrec rows. + * + * @return true iff the last row sequence overflowed + */ + public boolean lastSequenceOverflowed() { + return overflow_; + } + + /** + * A RowSequence wrapper that modifies obscore access URLs in result sets. + * This class intercepts access_url column values and rewrites them to use a different base URL + * + * @throws RuntimeException if URL rewriting fails due to malformed URLs + */ + private static class ModifiedLimitRowSequence extends WrapperRowSequence { + private final Map<Integer, Boolean> accessUrlColumns; + + ModifiedLimitRowSequence(RowSequence baseSeq, ColumnInfo[] columnInfos) { + super(baseSeq); + this.accessUrlColumns = findAccessUrlColumns(columnInfos); + } + + /** + * Find all access_url columns and determine if they belong to ivoa.ObsCore. + * Returns a map of column indices to boolean indicating if they should be modified. + */ + private static Map<Integer, Boolean> findAccessUrlColumns(ColumnInfo[] columnInfos) { + Map<Integer, Boolean> columns = new HashMap<>(); + try { + for (int i = 0; i < columnInfos.length; i++) { + ColumnInfo info = columnInfos[i]; + + DescribedValue tableNameValue = info.getAuxDatumByName(TABLE_NAME_INFO); + if (tableNameValue != null) { + String tableName = tableNameValue.getValue().toString(); + + if ("access_url".equals(info.getName())) { + boolean isObsCore = "ivoa.ObsCore".equals(tableName); + columns.put(i, isObsCore); + log.debug("Found access_url column at index " + i + + " for table " + tableName + + (isObsCore ? " (will modify URLs)" : " (won't modify URLs)")); + } + } + } + } catch (NullPointerException ex) { + log.debug("Error while trying to find access_url columns"); + } + + return columns; + } + + /** + * Returns the value from a single cell, modifying it if it's a URL that needs to be rewritten. + * + * @param icol the column index + * @return the cell contents + */ + @Override + public Object getCell(int icol) throws IOException { + Object value = super.getCell(icol); + if (shouldModifyUrl(icol, value)) { + return modifyAccessUrl((String) value); + } + return value; + } + + /** + * Returns values from an entire row, modifying any URLs that need to be rewritten. + * + * @return array containing values for the current row + */ + @Override + public Object[] getRow() throws IOException { + Object[] row = super.getRow(); + for (Map.Entry<Integer, Boolean> entry : accessUrlColumns.entrySet()) { + int index = entry.getKey(); + if (index >= 0 && index < row.length && + shouldModifyUrl(index, row[index])) { + row[index] = modifyAccessUrl((String) row[index]); + } + } + return row; + } + + /** + * Determines whether a value at a given index should have its URL modified. + * + * @param columnIndex index of the column containing the value + * @param value the value to check + * @return true if the value should have its URL modified + */ + private boolean shouldModifyUrl(int columnIndex, Object value) { + return value instanceof String && + accessUrlColumns.containsKey(columnIndex) && + accessUrlColumns.get(columnIndex) && + value != null; + } + + /** + * Modifies a URL to use the base URL while preserving the path. + * + * @param url the original URL to modify, or null + * @return the modified URL, or null if the input was null + * @throws RuntimeException if URL parsing or rewriting fails + */ + private String modifyAccessUrl(String url) { + if (url != null) { + String s = (String) url; + try { + URL orig = new URL(s); + URL base_url = new URL(BASE_URL); + URL rewritten = new URL(orig.getProtocol(), base_url.getHost(), orig.getFile()); + log.debug( "Rewritten URL: " + rewritten.toExternalForm()); + + return rewritten.toExternalForm(); + } catch (MalformedURLException ex) { + throw new RuntimeException("BUG: Failed to rewrite URL: " + s, ex); + } + } + return url; + } + } + + /** + * Returns a row sequence that may be limited to a maximum number of rows. + * Creates a sequence that wraps the underlying ResultSet, applies URL modifications + * if needed, and enforces any row limit specified by maxrec_. + * + * @return a row sequence, possibly truncated at maxrec_ rows + * @throws IOException if there is an error reading from the base sequence + */ + @Override + public RowSequence getRowSequence() throws IOException { + overflow_ = false; + totalRows = 0; + RowSequence baseSeq = super.getRowSequence(); + baseSeq = new ModifiedLimitRowSequence(baseSeq, columnInfos); + + if (maxrec_ < 0 || maxrec_ == Long.MAX_VALUE) { + return new WrapperRowSequence(baseSeq) { + @Override + public boolean next() throws IOException { + boolean hasNext = super.next(); + if (hasNext) { + totalRows++; + } + return hasNext; + } + }; + } else { + return new WrapperRowSequence( baseSeq ) { + long irow = -1; + @Override + public boolean next() throws IOException { + irow++; + if ( irow < maxrec_ ) { + boolean hasNext = super.next(); + if (hasNext) { + totalRows++; + } + return hasNext; + } + if ( irow == maxrec_ ) { + log.debug("Overflow reached while processing rows!"); + overflow_ = super.next(); + } + return false; + } + }; + } + } + } + + /** + * Creates a JDOM Element representing a VOTable RESOURCE from a VOTableResource object. + * Converts the VOTableResource into XML format, including all its attributes, description, + * INFO elements, parameters, and groups. + * + * @param votResource the VOTableResource to convert + * @param namespace the XML namespace to use, may be null + * @return JDOM Element containing the XML representation of the resource + */ + private Element createResource(VOTableResource votResource, Namespace namespace) { + // Create the RESOURCE element and add to the VOTABLE element. + Element resource = new Element("RESOURCE", namespace); + + resource.setAttribute("type", votResource.getType()); + log.debug("wrote resource.type: " + votResource.getType()); + + if (votResource.id != null) { + resource.setAttribute("ID", votResource.id); + } + + if (votResource.getName() != null) { + resource.setAttribute("name", votResource.getName()); + } + + if (votResource.utype != null) { + resource.setAttribute("utype", votResource.utype); + } + + // Create the DESCRIPTION element and add to RESOURCE element. + if (votResource.description != null) { + Element description = new Element("DESCRIPTION", namespace); + description.setText(votResource.description); + resource.addContent(description); + } + + // Create the INFO element and add to the RESOURCE element. + for (VOTableInfo in : votResource.getInfos()) { + Element info = new Element("INFO", namespace); + info.setAttribute("name", in.getName()); + info.setAttribute("value", in.getValue()); + if (in.content != null) { + info.setText(in.content); + } + resource.addContent(info); + } + log.debug("wrote resource.info: " + votResource.getInfos().size()); + + for (VOTableParam param : votResource.getParams()) { + resource.addContent(new ParamElement(param, namespace)); + } + log.debug("wrote resource.param: " + votResource.getParams().size()); + + for (VOTableGroup vg : votResource.getGroups()) { + resource.addContent(new GroupElement(vg, namespace)); + } + + return resource; + } + + /** + * Builds a empty VOTable document with the appropriate namespaces and + * attributes. + * + * @return VOTable document. + */ + protected Document createDocument() { + // the root VOTABLE element + Namespace vot = Namespace.getNamespace(VOTABLE_14_NS_URI); + Namespace xsi = Namespace.getNamespace("xsi", XSI_SCHEMA); + Element votable = new Element("VOTABLE", vot); + votable.setAttribute("version", VOTABLE_VERSION); + votable.addNamespaceDeclaration(xsi); + + Document document = new Document(); + document.addContent(votable); + + return document; + } + + /** + * Builds a string containing all exception messages from a throwable and its causes. + * + * @param thrown the throwable to process + * @return space-separated string of all exception messages in the chain + */ + private String getThrownExceptions(Throwable thrown) { + StringBuilder sb = new StringBuilder(); + if (thrown.getMessage() == null) { + sb.append(""); + } else { + sb.append(thrown.getMessage()); + } + while (thrown.getCause() != null) { + thrown = thrown.getCause(); + sb.append(" "); + if (thrown.getMessage() == null) { + sb.append(""); + } else { + sb.append(thrown.getMessage()); + } + } + return sb.toString(); + } + + +} diff --git a/src/main/java/org/opencadc/tap/impl/RubinFormatFactory.java b/src/main/java/org/opencadc/tap/impl/RubinFormatFactory.java index 630a2ac..29d4038 100644 --- a/src/main/java/org/opencadc/tap/impl/RubinFormatFactory.java +++ b/src/main/java/org/opencadc/tap/impl/RubinFormatFactory.java @@ -23,7 +23,7 @@ public Format<Object> getClobFormat(TapSelectItem columnDesc) { // ivoa.ObsCore if ("ivoa.ObsCore".equalsIgnoreCase(columnDesc.tableName)) { if ("access_url".equalsIgnoreCase(columnDesc.getColumnName())) { - log.info("getClobFormat called for access_url"); + log.info("getClobFormat called for access_url"); return new RubinURLFormat(); } } diff --git a/src/main/java/org/opencadc/tap/impl/RubinTableWriter.java b/src/main/java/org/opencadc/tap/impl/RubinTableWriter.java index ca6faee..e36005c 100644 --- a/src/main/java/org/opencadc/tap/impl/RubinTableWriter.java +++ b/src/main/java/org/opencadc/tap/impl/RubinTableWriter.java @@ -69,7 +69,6 @@ package org.opencadc.tap.impl; -import ca.nrc.cadc.auth.AuthMethod; import ca.nrc.cadc.auth.AuthenticationUtil; import ca.nrc.cadc.dali.tables.ascii.AsciiTableWriter; import ca.nrc.cadc.dali.tables.votable.VOTableDocument; @@ -80,6 +79,8 @@ import ca.nrc.cadc.dali.tables.votable.VOTableResource; import ca.nrc.cadc.dali.tables.votable.VOTableTable; import ca.nrc.cadc.dali.tables.votable.VOTableWriter; + +import org.opencadc.tap.impl.ResultSetWriter; import ca.nrc.cadc.dali.util.Format; import ca.nrc.cadc.date.DateUtil; import ca.nrc.cadc.reg.client.RegistryClient; @@ -91,6 +92,10 @@ import ca.nrc.cadc.tap.writer.format.FormatFactory; import ca.nrc.cadc.uws.Job; import ca.nrc.cadc.uws.ParameterUtil; +import uk.ac.starlink.table.ColumnInfo; +import uk.ac.starlink.table.DefaultValueInfo; +import uk.ac.starlink.table.DescribedValue; +import uk.ac.starlink.votable.VOStarTable; import java.io.BufferedWriter; import java.io.IOException; import java.io.OutputStream; @@ -123,6 +128,7 @@ public class RubinTableWriter implements TableWriter { private static final Logger log = Logger.getLogger(RubinTableWriter.class); + public static final String TABLE_NAME_INFO = "TABLE_NAME"; private static final String FORMAT = "RESPONSEFORMAT"; private static final String FORMAT_ALT = "FORMAT"; @@ -134,11 +140,14 @@ public class RubinTableWriter implements TableWriter public static final String TEXT = "text"; public static final String TSV = "tsv"; public static final String VOTABLE = "votable"; + public static final String VOTABLE_TD = "votable-td"; public static final String RSS = "rss"; // content-type // private static final String APPLICATION_FITS = "application/fits"; private static final String APPLICATION_VOTABLE_XML = "application/x-votable+xml"; + private static final String APPLICATION_VOTABLE_B2 = "application/x-votable+xml;serialization=binary2"; + private static final String APPLICATION_VOTABLE_TD_XML = "application/x-votable+xml;serialization=tabledata"; private static final String APPLICATION_RSS = "application/rss+xml"; private static final String TEXT_XML_VOTABLE = "text/xml;content=x-votable"; // the SIAv1 mimetype private static final String TEXT_CSV = "text/csv"; @@ -155,6 +164,8 @@ public class RubinTableWriter implements TableWriter static { knownFormats.put(APPLICATION_VOTABLE_XML, VOTABLE); + knownFormats.put(APPLICATION_VOTABLE_B2, VOTABLE); + knownFormats.put(APPLICATION_VOTABLE_TD_XML, VOTABLE_TD); knownFormats.put(TEXT_XML, VOTABLE); knownFormats.put(TEXT_XML_VOTABLE, VOTABLE); knownFormats.put(TEXT_CSV, CSV); @@ -172,8 +183,9 @@ public class RubinTableWriter implements TableWriter private String extension; // RssTableWriter not yet ported to cadcDALI - private ca.nrc.cadc.dali.tables.TableWriter<VOTableDocument> tableWriter; private RssTableWriter rssTableWriter; + private ca.nrc.cadc.dali.tables.TableWriter<ResultSet> resultSetWriter; + private ca.nrc.cadc.dali.tables.TableWriter<VOTableDocument> voDocumentWriter; private FormatFactory formatFactory; private boolean errorWriter = false; @@ -191,7 +203,7 @@ public RubinTableWriter() { public RubinTableWriter(boolean errorWriter) { this.errorWriter = errorWriter; } - + @Override public void setJob(Job job) { @@ -216,17 +228,26 @@ public void setQueryInfo(String queryInfo) @Override public String getContentType() { - return tableWriter.getContentType(); + String contentType = null; + + if (resultSetWriter != null) { + contentType = resultSetWriter.getContentType(); + } else if (resultSetWriter != null) { + contentType = resultSetWriter.getContentType(); + } + + return contentType; } @Override public String getErrorContentType() { - return tableWriter.getErrorContentType(); + return resultSetWriter.getErrorContentType(); } /** * Get the number of rows the output table + * * @return number of result rows written in output table */ @Override @@ -264,26 +285,44 @@ private void initFormat() // Note: This needs to be done before the write method is called so the contentType // can be determined from the table writer. - if (type.equals(RSS)) - { + switch (type) { + case RSS: rssTableWriter = new RssTableWriter(); rssTableWriter.setJob(job); - // for error handling - tableWriter = new AsciiTableWriter(AsciiTableWriter.ContentType.TSV); - } - else if (type.equals(VOTABLE)) { - tableWriter = new VOTableWriter(format); - } else if (type.equals(CSV)) { - tableWriter = new AsciiTableWriter(AsciiTableWriter.ContentType.CSV); - } else if (type.equals(TSV)) { - tableWriter = new AsciiTableWriter(AsciiTableWriter.ContentType.TSV); - } - - if (tableWriter == null) { + // For error handling + voDocumentWriter = new AsciiTableWriter(AsciiTableWriter.ContentType.TSV); + break; + + case VOTABLE: + resultSetWriter = new ResultSetWriter(); + this.contentType = resultSetWriter.getContentType(); + this.extension = resultSetWriter.getExtension(); + break; + + case VOTABLE_TD: + voDocumentWriter = new VOTableWriter(); + this.contentType = voDocumentWriter.getContentType(); + this.extension = voDocumentWriter.getExtension(); + break; + + case CSV: + voDocumentWriter = new AsciiTableWriter(AsciiTableWriter.ContentType.CSV); + break; + + case TSV: + voDocumentWriter = new AsciiTableWriter(AsciiTableWriter.ContentType.TSV); + break; + + default: throw new UnsupportedOperationException("unsupported format: " + type); - } - this.contentType = tableWriter.getContentType(); - this.extension = tableWriter.getExtension(); + } + + if (voDocumentWriter != null) { + this.contentType = voDocumentWriter.getContentType(); + this.extension = voDocumentWriter.getExtension(); + } + + } public void setFormatFactory(FormatFactory formatFactory) @@ -300,22 +339,29 @@ public void setFormatFactory(ca.nrc.cadc.dali.util.FormatFactory formatFactory) public void write(Throwable t, OutputStream out) throws IOException { - tableWriter.write(t, out); + if (this.resultSetWriter != null) { + this.resultSetWriter.write(t, out); + } else if (this.voDocumentWriter != null) { + this.voDocumentWriter.write(t, out); + } } @Override public void write(ResultSet rs, OutputStream out) throws IOException { - this.write(rs, out, null); + try (Writer writer = new BufferedWriter(new OutputStreamWriter(out, "UTF-8"))) { + this.write(rs, writer, null); + } } @Override public void write(ResultSet rs, OutputStream out, Long maxrec) throws IOException { - Writer writer = new BufferedWriter(new OutputStreamWriter(out, "UTF-8")); - this.write(rs, writer, maxrec); + try (Writer writer = new BufferedWriter(new OutputStreamWriter(out, "UTF-8"))) { + write(rs, writer, maxrec); + } } @Override @@ -354,6 +400,8 @@ public void write(ResultSet rs, Writer out, Long maxrec) throws IOException List<String> columnNames = new ArrayList<String>(); int listIndex = 0; + + List<ColumnInfo> columnInfoList = new ArrayList<>(); // Add the metadata elements. for (TapSelectItem resultCol : selectList) @@ -367,7 +415,20 @@ public void write(ResultSet rs, Writer out, Long maxrec) throws IOException String fullColumnName = resultCol.tableName + "_" + resultCol.getColumnName(); columnNames.add(fullColumnName.replace(".", "_")); listIndex++; + + // Generate a ColumnInfo list, to be used by ResultSetWriter for generating the field metadata + ColumnInfo colInfo = new ColumnInfo(resultCol.getColumnName(), getDatatypeClass(resultCol.getDatatype(), newField.getArraysize()), newField.description); + colInfo.setUCD(resultCol.ucd); + colInfo.setUtype(resultCol.utype); + colInfo.setUnitString(resultCol.unit); + colInfo.setXtype(newField.xtype); + colInfo.setAuxDatum(new DescribedValue(VOStarTable.ID_INFO, newField.id)); + colInfo.setAuxDatum(new DescribedValue(new DefaultValueInfo(TABLE_NAME_INFO, String.class), + resultCol.tableName)); + columnInfoList.add(colInfo); + } + ColumnInfo[] columnInfoArray = columnInfoList.toArray(new ColumnInfo[0]); List<String> serviceIDs = determineDatalinks(columnNames); @@ -376,7 +437,9 @@ public void write(ResultSet rs, Writer out, Long maxrec) throws IOException // Add the "meta" resources to describe services for each columnID in // list columnIDs that we recognize - addMetaResources(votableDocument, serviceIDs, columnNames); + + List<VOTableResource> metaResources = generateMetaResources(serviceIDs, columnNames); + votableDocument.getResources().addAll(metaResources); VOTableInfo info = new VOTableInfo("QUERY_STATUS", "OK"); resultsResource.getInfos().add(info); @@ -392,31 +455,95 @@ public void write(ResultSet rs, Writer out, Long maxrec) throws IOException info = new VOTableInfo("QUERY", queryInfo); resultsResource.getInfos().add(info); } + + if (resultSetWriter != null) { + ((ResultSetWriter) resultSetWriter).setInfos(resultsResource.getInfos()); + ((ResultSetWriter) resultSetWriter).setResources(metaResources); + ((ResultSetWriter) resultSetWriter).setColumns(columnInfoArray); + if (maxrec != null) { + resultSetWriter.write(rs, out, maxrec); + } else { + resultSetWriter.write(rs, out); + } + this.rowcount = ((ResultSetWriter) resultSetWriter).getRowCount(); + + } else if (voDocumentWriter != null) { + ResultSetTableData tableData = new ResultSetTableData(rs, formats); + resultsTable.setTableData(tableData); + if (maxrec != null) { + voDocumentWriter.write(votableDocument, out, maxrec); + } else { + voDocumentWriter.write(votableDocument, out); + } + this.rowcount = tableData.getRowCount(); + } + + log.debug("Final row count after processing: " + this.rowcount); - ResultSetTableData tableData = new ResultSetTableData(rs, formats); - resultsTable.setTableData(tableData); - - if (maxrec != null) - tableWriter.write(votableDocument, out, maxrec); - else - tableWriter.write(votableDocument, out); - - this.rowcount = tableData.getRowCount(); } - - private void addMetaResources(VOTableDocument votableDocument, List<String> serviceIDs, List<String> columns) - throws IOException - { - for (String serviceID : serviceIDs) - { + + /** + * Determines the appropriate Java class type for a given TAP data type and array size specification. + * + * @param datatype The TAP data type specification object containing the type information. + * @param arraysize The size specification for array types. + * + * @return The corresponding Java Class object that represents the specified data type. + * + * @throws NullPointerException if datatype is null + */ + protected static final Class<?> getDatatypeClass(final TapDataType datatype, final String arraysize) { + boolean isScalar = arraysize == null || (arraysize.length() == 1 && arraysize.equals("1")); + switch(datatype.getDatatype().toUpperCase()) { + case "BLOB": + return boolean[].class; + case "BOOLEAN": + return isScalar ? Boolean.class : boolean[].class; + case "DOUBLE": + return isScalar ? Double.class : double[].class; + case "DOUBLECOMPLEX": + return double[].class; + case "FLOAT": + return isScalar ? Float.class : float[].class; + case "FLOATCOMPLEX": + return float[].class; + case "INT": + return isScalar ? Integer.class : int[].class; + case "LONG": + return isScalar ? Long.class : long[].class; + case "SHORT": + return isScalar ? Short.class : short[].class; + case "UNSIGNEDBYTE": + return isScalar ? Short.class : short[].class; + case "CHAR": + case "UNICODECHAR": + default: /* If the type is not know (theoretically, never happens), return char[*] by default. */ + return isScalar ? Character.class : String.class; + } + } + + /** + * Generates a list of VOTable meta resources. + * + * @param serviceIDs A list of service identifiers used to locate corresponding XML templates. + * + * @param columns A list of column names that will be mapped to template variables. + * + * @return A list of VOTableResource objects containing the processed meta information + * + * @throws IOException + * @throws IllegalStateException + */ + private List<VOTableResource> generateMetaResources(List<String> serviceIDs, List<String> columns) throws IOException { + List<VOTableResource> metaResources = new ArrayList<>(); + for (String serviceID : serviceIDs) { Path snippetPath = Path.of(datalinkConfig + serviceID + ".xml"); String content = Files.readString(snippetPath, StandardCharsets.US_ASCII); ST datalinkTemplate = new ST(content, '$', '$'); datalinkTemplate.add("baseUrl", baseUrl); int columnIndex = 0; - for (String col : columns) - { + for (String col : columns) { datalinkTemplate.add(col, "col_" + columnIndex); columnIndex++; } @@ -424,8 +551,9 @@ private void addMetaResources(VOTableDocument votableDocument, List<String> serv VOTableReader reader = new VOTableReader(); VOTableDocument serviceDocument = reader.read(datalinkTemplate.render()); VOTableResource metaResource = serviceDocument.getResourceByType("meta"); - votableDocument.getResources().add(metaResource); + metaResources.add(metaResource); } + return metaResources; } private List<String> determineDatalinks(List<String> columns) From e3dafcc90136d582bc5aaeeb44f076b8b35e643f Mon Sep 17 00:00:00 2001 From: Stelios Voutsinas <steliosvoutsinas@yahoo.com> Date: Tue, 14 Jan 2025 15:35:56 -0700 Subject: [PATCH 2/3] Bugfix for column aliases --- .../java/org/opencadc/tap/impl/ResultSetWriter.java | 5 ++++- .../java/org/opencadc/tap/impl/RubinTableWriter.java | 10 +++++++--- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/opencadc/tap/impl/ResultSetWriter.java b/src/main/java/org/opencadc/tap/impl/ResultSetWriter.java index b4ab6c5..4c65d29 100644 --- a/src/main/java/org/opencadc/tap/impl/ResultSetWriter.java +++ b/src/main/java/org/opencadc/tap/impl/ResultSetWriter.java @@ -396,6 +396,7 @@ private static class LimitedResultSetStarTable extends SequentialResultSetStarTable { public static final String TABLE_NAME_INFO = "TABLE_NAME"; + public static final String ACTUAL_COLUMN_NAME_INFO = "COLUMN_NAME"; private final long maxrec_; private boolean overflow_; private long totalRows = 0; @@ -468,10 +469,12 @@ private static Map<Integer, Boolean> findAccessUrlColumns(ColumnInfo[] columnInf ColumnInfo info = columnInfos[i]; DescribedValue tableNameValue = info.getAuxDatumByName(TABLE_NAME_INFO); + DescribedValue actualColumnValue = info.getAuxDatumByName(ACTUAL_COLUMN_NAME_INFO); if (tableNameValue != null) { String tableName = tableNameValue.getValue().toString(); + String actualColName = actualColumnValue.getValue().toString(); - if ("access_url".equals(info.getName())) { + if ("access_url".equals(actualColName)) { boolean isObsCore = "ivoa.ObsCore".equals(tableName); columns.put(i, isObsCore); log.debug("Found access_url column at index " + i + diff --git a/src/main/java/org/opencadc/tap/impl/RubinTableWriter.java b/src/main/java/org/opencadc/tap/impl/RubinTableWriter.java index e36005c..b62c27a 100644 --- a/src/main/java/org/opencadc/tap/impl/RubinTableWriter.java +++ b/src/main/java/org/opencadc/tap/impl/RubinTableWriter.java @@ -129,6 +129,7 @@ public class RubinTableWriter implements TableWriter private static final Logger log = Logger.getLogger(RubinTableWriter.class); public static final String TABLE_NAME_INFO = "TABLE_NAME"; + public static final String ACTUAL_COLUMN_NAME_INFO = "COLUMN_NAME"; private static final String FORMAT = "RESPONSEFORMAT"; private static final String FORMAT_ALT = "FORMAT"; @@ -415,9 +416,8 @@ public void write(ResultSet rs, Writer out, Long maxrec) throws IOException String fullColumnName = resultCol.tableName + "_" + resultCol.getColumnName(); columnNames.add(fullColumnName.replace(".", "_")); listIndex++; - // Generate a ColumnInfo list, to be used by ResultSetWriter for generating the field metadata - ColumnInfo colInfo = new ColumnInfo(resultCol.getColumnName(), getDatatypeClass(resultCol.getDatatype(), newField.getArraysize()), newField.description); + ColumnInfo colInfo = new ColumnInfo(resultCol.getName(), getDatatypeClass(resultCol.getDatatype(), newField.getArraysize()), newField.description); colInfo.setUCD(resultCol.ucd); colInfo.setUtype(resultCol.utype); colInfo.setUnitString(resultCol.unit); @@ -425,9 +425,13 @@ public void write(ResultSet rs, Writer out, Long maxrec) throws IOException colInfo.setAuxDatum(new DescribedValue(VOStarTable.ID_INFO, newField.id)); colInfo.setAuxDatum(new DescribedValue(new DefaultValueInfo(TABLE_NAME_INFO, String.class), resultCol.tableName)); - columnInfoList.add(colInfo); + colInfo.setAuxDatum(new DescribedValue(new DefaultValueInfo(ACTUAL_COLUMN_NAME_INFO, String.class), + resultCol.getColumnName())); + + columnInfoList.add(colInfo); } + ColumnInfo[] columnInfoArray = columnInfoList.toArray(new ColumnInfo[0]); List<String> serviceIDs = determineDatalinks(columnNames); From 73f6996aa8f242eef1ee3178e8651af1ba7a6bd7 Mon Sep 17 00:00:00 2001 From: Stelios Voutsinas <steliosvoutsinas@yahoo.com> Date: Wed, 15 Jan 2025 11:32:29 -0700 Subject: [PATCH 3/3] Workaround for maxrec=0 issue & char fields --- .../opencadc/tap/impl/ResultSetWriter.java | 243 +++++++++++++++--- .../opencadc/tap/impl/RubinTableWriter.java | 34 ++- 2 files changed, 243 insertions(+), 34 deletions(-) diff --git a/src/main/java/org/opencadc/tap/impl/ResultSetWriter.java b/src/main/java/org/opencadc/tap/impl/ResultSetWriter.java index 4c65d29..852991d 100644 --- a/src/main/java/org/opencadc/tap/impl/ResultSetWriter.java +++ b/src/main/java/org/opencadc/tap/impl/ResultSetWriter.java @@ -57,7 +57,7 @@ public class ResultSetWriter implements TableWriter<ResultSet> { public static final String CONTENT_TYPE_ALT = "text/xml"; // VOTable Version number. - public static final String VOTABLE_VERSION = "1.4"; + public static final String VOTABLE_VERSION = "1.3"; // Uri to the XML schema. public static final String XSI_SCHEMA = "http://www.w3.org/2001/XMLSchema-instance"; @@ -91,7 +91,7 @@ public long getRowCount() { * Default constructor. */ public ResultSetWriter() { - this(null, uk.ac.starlink.votable.DataFormat.BINARY2, VOTableVersion.V14, -1); + this(null, uk.ac.starlink.votable.DataFormat.BINARY2, VOTableVersion.V13, -1); } @@ -325,6 +325,12 @@ protected void writeImpl(ResultSet resultSet, Writer writer, Long maxrec) ? (BufferedWriter) writer : new BufferedWriter(writer)) { + + if (maxrec != null && maxrec == 0 || resultSet == null) { + writeEmptyResult(out); + return; + } + LimitedResultSetStarTable table; try { table = new LimitedResultSetStarTable(this.getColumns(), resultSet, maxrec); @@ -344,7 +350,7 @@ protected void writeImpl(ResultSet resultSet, Writer writer, Long maxrec) version_.getXmlNamespace() ) + ">" ); out.newLine(); - out.write( "<RESOURCE>" ); + out.write( "<RESOURCE type='results'>" ); out.newLine(); XMLOutputter outputter = new XMLOutputter(); @@ -384,6 +390,136 @@ protected void writeImpl(ResultSet resultSet, Writer writer, Long maxrec) } } + + /** + * Write out an empty result (Used when maxrec=0) + * + * @param out + */ + void writeEmptyResult(BufferedWriter out) { + try { + /* Write header. */ + out.write("<VOTABLE" + + VOSerializer.formatAttribute("version", version_.getVersionNumber()) + + VOSerializer.formatAttribute("xmlns", version_.getXmlNamespace()) + + ">"); + out.newLine(); + out.write("<RESOURCE type='results'>"); + out.newLine(); + + XMLOutputter outputter = new XMLOutputter(); + outputter.setFormat(org.jdom2.output.Format.getPrettyFormat()); + + // Write all info elements + for (VOTableInfo info : infos) { + Element infoElement = new Element("INFO"); + infoElement.setAttribute("name", info.getName()); + infoElement.setAttribute("value", info.getValue()); + outputter.output(infoElement, out); + out.newLine(); + } + + // Add TABLE element and column metadata + out.write("<TABLE>\n"); + + // Write all column metadata + for (ColumnInfo colInfo : columns) { + Element field = new Element("FIELD"); + field.setAttribute("name", colInfo.getName()); + + // Set datatype + if (colInfo.getContentClass() != null) { + String datatype = getVOTableDatatype(colInfo.getContentClass()); + if (datatype != null) { + field.setAttribute("datatype", datatype); + + // Handle array types + if (colInfo.getContentClass().isArray()) { + if (colInfo.getContentClass().getComponentType() == String.class) { + field.setAttribute("arraysize", "*"); + } else if (colInfo.getShape() != null && colInfo.getShape().length > 0) { + field.setAttribute("arraysize", String.valueOf(colInfo.getShape()[0])); + } + } + } + } + + // Add standard metadata + if (colInfo.getUnitString() != null) { + field.setAttribute("unit", colInfo.getUnitString()); + } + if (colInfo.getUCD() != null) { + field.setAttribute("ucd", colInfo.getUCD()); + } + if (colInfo.getUtype() != null) { + field.setAttribute("utype", colInfo.getUtype()); + } + + // Add ID if present + DescribedValue idValue = colInfo.getAuxDatumByName("ID_INFO"); + if (idValue != null && idValue.getValue() != null) { + field.setAttribute("ID", idValue.getValue().toString()); + } + + // Add description if present + String description = colInfo.getDescription(); + if (description != null && !description.isEmpty()) { + Element desc = new Element("DESCRIPTION"); + desc.setText(description); + field.addContent(desc); + } + + outputter.output(field, out); + out.newLine(); + } + + // Write the empty data section + out.write("<DATA>\n"); + out.write("<BINARY2>\n"); + out.write("<STREAM encoding='base64'>\n"); + out.write("</STREAM>\n"); + out.write("</BINARY2>\n"); + out.write("</DATA>\n"); + + out.write("</TABLE>\n"); // Close the TABLE element + + /* Write footer. */ + out.write("</RESOURCE>"); + out.newLine(); + for (VOTableResource resource : resources) { + Element r = createResource(resource, null); + outputter.output(r, out); + out.newLine(); + } + out.write("</VOTABLE>"); + out.newLine(); + out.flush(); + + } catch (IOException e) { + e.printStackTrace(); + } + + this.totalRows = 0; + } + + /** + * Convert Java class to VOTable datatype + * + * @param clazz + * @return + */ + private String getVOTableDatatype(Class<?> clazz) { + if (clazz == Boolean.class || clazz == boolean.class) return "boolean"; + if (clazz == Byte.class || clazz == byte.class) return "unsignedByte"; + if (clazz == Short.class || clazz == short.class) return "short"; + if (clazz == Integer.class || clazz == int.class) return "int"; + if (clazz == Long.class || clazz == long.class) return "long"; + if (clazz == Float.class || clazz == float.class) return "float"; + if (clazz == Double.class || clazz == double.class) return "double"; + if (clazz == Character.class || clazz == char.class) return "char"; + if (clazz == String.class) return "char"; + return null; + } /** * StarTable implementation which is based on a ResultSet, and which @@ -452,10 +588,12 @@ public boolean lastSequenceOverflowed() { */ private static class ModifiedLimitRowSequence extends WrapperRowSequence { private final Map<Integer, Boolean> accessUrlColumns; - + private ColumnInfo[] columnInfos; + ModifiedLimitRowSequence(RowSequence baseSeq, ColumnInfo[] columnInfos) { super(baseSeq); this.accessUrlColumns = findAccessUrlColumns(columnInfos); + this.columnInfos = columnInfos; } /** @@ -477,9 +615,6 @@ private static Map<Integer, Boolean> findAccessUrlColumns(ColumnInfo[] columnInf if ("access_url".equals(actualColName)) { boolean isObsCore = "ivoa.ObsCore".equals(tableName); columns.put(i, isObsCore); - log.debug("Found access_url column at index " + i + - " for table " + tableName + - (isObsCore ? " (will modify URLs)" : " (won't modify URLs)")); } } } @@ -491,38 +626,83 @@ private static Map<Integer, Boolean> findAccessUrlColumns(ColumnInfo[] columnInf } /** - * Returns the value from a single cell, modifying it if it's a URL that needs to be rewritten. + * Return values from a row, applying necessary type conversions and URL modifications. * - * @param icol the column index - * @return the cell contents + * This method handles two types of transformations: + * 1. String to Character conversion for VOTable char datatype fields + * 2. URL rewriting for access_url columns in ObsCore tables + * + * For char datatype columns: + * - If the input is a string and the column expects char, take the first character + * - Empty strings are converted to null + * - Non-string values are passed through unchanged + * + * @return array containing values for the current row, with appropriate type conversions + * @throws IOException if there is an error reading from the underlying sequence */ @Override - public Object getCell(int icol) throws IOException { - Object value = super.getCell(icol); - if (shouldModifyUrl(icol, value)) { - return modifyAccessUrl((String) value); + public Object[] getRow() throws IOException { + Object[] row = super.getRow(); + if (row == null) { + return null; } - return value; + + for (int i = 0; i < row.length; i++) { + Object value = row[i]; + ColumnInfo info = columnInfos[i]; + + if (value != null) { + + // Handle conversion from String to Character for char columns + if (value instanceof String && info.getContentClass() == Character.class) { + String strVal = (String)value; + if (strVal.length() > 0) { + row[i] = strVal.charAt(0); + } else { + row[i] = null; + } + } + } + + if (shouldModifyUrl(i, row[i])) { + row[i] = modifyAccessUrl((String)row[i]); + } + } + + return row; } /** - * Returns values from an entire row, modifying any URLs that need to be rewritten. + * Returns the value from a single cell, applying necessary type conversions and URL modifications. * - * @return array containing values for the current row + * @param icol the column index + * @return the cell contents after any necessary conversions + * @throws IOException if there is an error reading from the underlying sequence */ @Override - public Object[] getRow() throws IOException { - Object[] row = super.getRow(); - for (Map.Entry<Integer, Boolean> entry : accessUrlColumns.entrySet()) { - int index = entry.getKey(); - if (index >= 0 && index < row.length && - shouldModifyUrl(index, row[index])) { - row[index] = modifyAccessUrl((String) row[index]); + public Object getCell(int icol) throws IOException { + Object value = super.getCell(icol); + ColumnInfo info = columnInfos[icol]; + + if (value != null) { + // Handle conversion from String to Character for char columns + if (value instanceof String && info.getContentClass() == Character.class) { + String strVal = (String)value; + if (strVal.length() > 0) { + value = strVal.charAt(0); + } else { + value = null; + } } } - return row; + + if (shouldModifyUrl(icol, value)) { + value = modifyAccessUrl((String)value); + } + + return value; } - + /** * Determines whether a value at a given index should have its URL modified. * @@ -552,12 +732,11 @@ private String modifyAccessUrl(String url) { URL base_url = new URL(BASE_URL); URL rewritten = new URL(orig.getProtocol(), base_url.getHost(), orig.getFile()); log.debug( "Rewritten URL: " + rewritten.toExternalForm()); - return rewritten.toExternalForm(); } catch (MalformedURLException ex) { - throw new RuntimeException("BUG: Failed to rewrite URL: " + s, ex); - } + throw new RuntimeException("BUG: Failed to rewrite URL: " + s, ex); } + } return url; } } @@ -679,7 +858,7 @@ private Element createResource(VOTableResource votResource, Namespace namespace) */ protected Document createDocument() { // the root VOTABLE element - Namespace vot = Namespace.getNamespace(VOTABLE_14_NS_URI); + Namespace vot = Namespace.getNamespace(VOTABLE_13_NS_URI); Namespace xsi = Namespace.getNamespace("xsi", XSI_SCHEMA); Element votable = new Element("VOTABLE", vot); votable.setAttribute("version", VOTABLE_VERSION); @@ -713,7 +892,9 @@ private String getThrownExceptions(Throwable thrown) { sb.append(thrown.getMessage()); } } - return sb.toString(); + String result = sb.toString().trim(); + return result.isEmpty() ? "An unknown error occurred" : result; + } diff --git a/src/main/java/org/opencadc/tap/impl/RubinTableWriter.java b/src/main/java/org/opencadc/tap/impl/RubinTableWriter.java index b62c27a..92b25a8 100644 --- a/src/main/java/org/opencadc/tap/impl/RubinTableWriter.java +++ b/src/main/java/org/opencadc/tap/impl/RubinTableWriter.java @@ -417,19 +417,26 @@ public void write(ResultSet rs, Writer out, Long maxrec) throws IOException columnNames.add(fullColumnName.replace(".", "_")); listIndex++; // Generate a ColumnInfo list, to be used by ResultSetWriter for generating the field metadata - ColumnInfo colInfo = new ColumnInfo(resultCol.getName(), getDatatypeClass(resultCol.getDatatype(), newField.getArraysize()), newField.description); + String colArraySize = newField.getArraysize(); + if (colArraySize == null && "CHAR".equals(newField.getDatatype().toUpperCase())) { + colArraySize = "1"; + } + + ColumnInfo colInfo = new ColumnInfo(resultCol.getName(), getDatatypeClass(resultCol.getDatatype(), colArraySize), newField.description); colInfo.setUCD(resultCol.ucd); colInfo.setUtype(resultCol.utype); colInfo.setUnitString(resultCol.unit); colInfo.setXtype(newField.xtype); + colInfo.setShape(getShape(colArraySize)); + + colInfo.setAuxDatum(new DescribedValue(VOStarTable.ID_INFO, newField.id)); colInfo.setAuxDatum(new DescribedValue(new DefaultValueInfo(TABLE_NAME_INFO, String.class), resultCol.tableName)); colInfo.setAuxDatum(new DescribedValue(new DefaultValueInfo(ACTUAL_COLUMN_NAME_INFO, String.class), resultCol.getColumnName())); - + columnInfoList.add(colInfo); - } ColumnInfo[] columnInfoArray = columnInfoList.toArray(new ColumnInfo[0]); @@ -526,6 +533,27 @@ protected static final Class<?> getDatatypeClass(final TapDataType datatype, fin } } + /** + * Convert the given VOTable arraysize into a {@link ColumnInfo} shape. + * + * @param arraysize Value of the VOTable attribute "arraysize". + * + * @return The corresponding {@link ColumnInfo} shape. + */ + protected static final int[] getShape(final String arraysize) { + if (arraysize == null) + return new int[0]; + else if (arraysize.charAt(arraysize.length() - 1) == '*') + return new int[]{ -1 }; + else { + try { + return new int[]{ Integer.parseInt(arraysize) }; + } catch(NumberFormatException nfe) { + return new int[0]; + } + } + } + /** * Generates a list of VOTable meta resources. *