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());
}
}
}