package tap.formatter; /* * This file is part of TAPLibrary. * * TAPLibrary is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * TAPLibrary is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with TAPLibrary. If not, see <http://www.gnu.org/licenses/>. * * Copyright 2012-2015 - UDS/Centre de DonnĂ©es astronomiques de Strasbourg (CDS), * Astronomisches Rechen Institut (ARI) */ import java.io.BufferedWriter; import java.io.IOException; import java.io.OutputStream; import java.io.OutputStreamWriter; import adql.db.DBColumn; import cds.util.AsciiTable; import tap.ServiceConnection; import tap.TAPException; import tap.TAPExecutionReport; import tap.data.TableIterator; /** * Format any given query (table) result into a simple table ASCII representation * (columns' width are adjusted so that all columns are well aligned and of the same width). * * @author Grégory Mantelet (CDS;ARI) * @version 2.0 (04/2015) */ public class TextFormat implements OutputFormat { /** Internal column separator. * Note: the output separator is however always a |. * @since 2.0 */ protected static final char COL_SEP = '\u25c6'; /** The {@link ServiceConnection} to use (for the log and to have some information about the service (particularly: name, description). */ protected final ServiceConnection service; /** * Build a {@link TextFormat}. * * @param service Description of the TAP service. * * @throws NullPointerException If the given service connection is <code>null</code>. */ public TextFormat(final ServiceConnection service) throws NullPointerException{ if (service == null) throw new NullPointerException("The given service connection is NULL!"); this.service = service; } @Override public String getMimeType(){ return "text/plain"; } @Override public String getShortMimeType(){ return "text"; } @Override public String getDescription(){ return null; } @Override public String getFileExtension(){ return "txt"; } @Override public void writeResult(TableIterator result, OutputStream output, TAPExecutionReport execReport, Thread thread) throws TAPException, IOException, InterruptedException{ // Prepare the formatting of the whole output: AsciiTable asciiTable = new AsciiTable(COL_SEP); // Write header: String headerLine = getHeader(result, execReport, thread); asciiTable.addHeaderLine(headerLine); asciiTable.endHeaderLine(); if (thread.isInterrupted()) throw new InterruptedException(); // Write data into the AsciiTable object: boolean overflow = writeData(result, asciiTable, execReport, thread); // Finally write the formatted ASCII table (header + data) in the output stream: BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(output)); String[] lines = asciiTable.displayAligned(new int[]{AsciiTable.LEFT}, '|'); execReport.nbRows = 0; for(String l : lines){ // stop right now the formatting if the job has been aborted/canceled/interrupted: if (thread.isInterrupted()) throw new InterruptedException(); // write the line: writer.write(l); writer.newLine(); // update the counter of written lines: execReport.nbRows++; // flush the writer every 30 lines: if (execReport.nbRows % 30 == 0) writer.flush(); } // Add a line in case of an OVERFLOW: if (overflow){ writer.write("\nOVERFLOW (more rows were available but have been truncated by the TAP service)"); writer.newLine(); } writer.flush(); } /** * Get the whole header (one row whose columns are just the columns' name). * * @param result Result to write later (but it contains also metadata that was extracted from the result itself). * @param execReport Execution report (which contains the metadata extracted/guessed from the ADQL query). * @param thread Thread which has asked for this formatting (it must be used in order to test the {@link Thread#isInterrupted()} flag and so interrupt everything if need). * * @return All the written metadata. * * @throws TAPException If any other error occurs. */ protected String getHeader(final TableIterator result, final TAPExecutionReport execReport, final Thread thread) throws TAPException{ // Get the columns meta: DBColumn[] selectedColumns = execReport.resultingColumns; StringBuffer line = new StringBuffer(); // If meta are not known, no header will be written: int nbColumns = (selectedColumns == null) ? -1 : selectedColumns.length; if (nbColumns > 0){ // Write all columns' name: for(int i = 0; i < nbColumns - 1; i++) line.append(selectedColumns[i].getADQLName()).append(COL_SEP); line.append(selectedColumns[nbColumns - 1].getADQLName()); } // Return the header line: return line.toString(); } /** * Write all the data rows into the given {@link AsciiTable} object. * * @param queryResult Result to write. * @param asciiTable Output in which the rows (as string) must be written. * @param execReport Execution report (which contains the maximum allowed number of records to output). * @param thread Thread which has asked for this formatting (it must be used in order to test the {@link Thread#isInterrupted()} flag and so interrupt everything if need). * * @return <i>true</i> if an overflow (i.e. nbDBRows > MAXREC) is detected, <i>false</i> otherwise. * * @throws InterruptedException If the thread has been interrupted. * @throws TAPException If any other error occurs. */ protected boolean writeData(final TableIterator queryResult, final AsciiTable asciiTable, final TAPExecutionReport execReport, final Thread thread) throws TAPException, InterruptedException{ execReport.nbRows = 0; boolean overflow = false; // Get the list of columns: DBColumn[] selectedColumns = execReport.resultingColumns; int nbColumns = selectedColumns.length; StringBuffer line = new StringBuffer(); while(queryResult.nextRow()){ // Stop right now the formatting if the job has been aborted/canceled/interrupted: if (thread.isInterrupted()) throw new InterruptedException(); // Deal with OVERFLOW, if needed: if (execReport.parameters.getMaxRec() > 0 && execReport.nbRows >= execReport.parameters.getMaxRec()){ overflow = true; break; } // Clear the line buffer: line.delete(0, line.length()); int indCol = 0; while(queryResult.hasNextCol()){ // Write the column value: writeFieldValue(queryResult.nextCol(), selectedColumns[indCol++], line); // Write the column separator (if needed): if (indCol != nbColumns) line.append(COL_SEP); } // Append the line/row in the ASCII table: asciiTable.addLine(line.toString()); execReport.nbRows++; } return overflow; } /** * Writes the given field value in the given buffer. * * @param value The value to write. * @param tapCol The corresponding column metadata. * @param line The buffer in which the field value must be written. */ protected void writeFieldValue(final Object value, final DBColumn tapCol, final StringBuffer line){ if (value != null){ if (value instanceof String) line.append('"').append(value.toString()).append('"'); else line.append(value.toString()); } } }