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 org.json.JSONException; import org.json.JSONWriter; import tap.ServiceConnection; import tap.TAPException; import tap.TAPExecutionReport; import tap.data.TableIterator; import tap.metadata.TAPColumn; import tap.metadata.VotType; import adql.db.DBColumn; import adql.db.DBType; import adql.db.DBType.DBDatatype; /** * Format any given query (table) result into JSON. * * @author Grégory Mantelet (CDS;ARI) * @version 2.1 (08/2015) */ public class JSONFormat implements OutputFormat { /** 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 JSON formatter. * * @param service Description of the TAP service. * * @throws NullPointerException If the given service connection is <code>null</code>. */ public JSONFormat(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 "application/json"; } @Override public String getShortMimeType(){ return "json"; } @Override public String getDescription(){ return null; } @Override public String getFileExtension(){ return "json"; } @Override public void writeResult(TableIterator result, OutputStream output, TAPExecutionReport execReport, Thread thread) throws TAPException, IOException, InterruptedException{ try{ // Prepare the output stream for JSON: BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(output)); JSONWriter out = new JSONWriter(writer); // { out.object(); // "metadata": [...] out.key("metadata"); // Write metadata part: DBColumn[] columns = writeMetadata(result, out, execReport, thread); writer.flush(); if (thread.isInterrupted()) throw new InterruptedException(); // "data": [...] out.key("data"); // Write the data part: writeData(result, columns, out, execReport, thread); // } out.endObject(); writer.flush(); }catch(JSONException je){ throw new TAPException(je.getMessage(), je); } } /** * Write the whole metadata part of the JSON file. * * @param result Result to write later (but it contains also metadata that was extracted from the result itself). * @param out Output stream in which the metadata must be written. * @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 IOException If there is an error while writing something in the output stream. * @throws InterruptedException If the thread has been interrupted. * @throws JSONException If there is an error while formatting something in JSON. * @throws TAPException If any other error occurs. * * @see #getValidColMeta(DBColumn, TAPColumn) */ protected DBColumn[] writeMetadata(TableIterator result, JSONWriter out, TAPExecutionReport execReport, Thread thread) throws IOException, TAPException, InterruptedException, JSONException{ out.array(); // Get the metadata extracted/guesses from the ADQL query: DBColumn[] columnsFromQuery = execReport.resultingColumns; // Get the metadata extracted from the result: TAPColumn[] columnsFromResult = result.getMetadata(); int indField = 0; if (columnsFromQuery != null){ // For each column: for(DBColumn field : columnsFromQuery){ // Try to build/get appropriate metadata for this field/column: TAPColumn colFromResult = (columnsFromResult != null && indField < columnsFromResult.length) ? columnsFromResult[indField] : null; TAPColumn tapCol = getValidColMeta(field, colFromResult); // Ensure these metadata are well returned at the end of this function: columnsFromQuery[indField] = tapCol; // Write the field/column metadata in the JSON output: writeFieldMeta(tapCol, out); indField++; } } out.endArray(); return columnsFromQuery; } /** * Try to get or otherwise to build appropriate metadata using those extracted from the ADQL query and those extracted from the result. * * @param typeFromQuery Metadata extracted/guessed from the ADQL query. * @param typeFromResult Metadata extracted/guessed from the result. * * @return The most appropriate metadata. */ protected TAPColumn getValidColMeta(final DBColumn typeFromQuery, final TAPColumn typeFromResult){ if (typeFromQuery != null && typeFromQuery instanceof TAPColumn){ TAPColumn colMeta = (TAPColumn)typeFromQuery; if (colMeta.getDatatype().isUnknown() && typeFromResult != null && !typeFromResult.getDatatype().isUnknown()) colMeta.setDatatype(typeFromResult.getDatatype()); return colMeta; }else if (typeFromResult != null){ if (typeFromQuery != null) return (TAPColumn)typeFromResult.copy(typeFromQuery.getDBName(), typeFromQuery.getADQLName(), null); else return (TAPColumn)typeFromResult.copy(); }else return new TAPColumn((typeFromQuery != null) ? typeFromQuery.getADQLName() : "?", new DBType(DBDatatype.VARCHAR), "?"); } /** * Formats in JSON and writes the given {@link TAPColumn} in the given output. * * @param tapCol The column metadata to format/write in JSON. * @param out The stream in which the formatted column metadata must be written. * * @throws IOException If there is an error while writing the field metadata. * @throws JSONException If there is an error while formatting something in JSON format. * @throws TAPException If there is any other error (by default: never happen). */ protected void writeFieldMeta(TAPColumn tapCol, JSONWriter out) throws IOException, TAPException, JSONException{ // { out.object(); // "name": "..." out.key("name").value(tapCol.getADQLName()); // "description": "..." (if any) if (tapCol.getDescription() != null && tapCol.getDescription().trim().length() > 0) out.key("description").value(tapCol.getDescription()); // "datatype": "..." VotType votType = new VotType(tapCol.getDatatype()); out.key("datatype").value(votType.datatype); // "arraysize": "..." (if any) if (votType.arraysize != null) out.key("arraysize").value(votType.arraysize); // "xtype": "..." (if any) if (votType.xtype != null) out.key("xtype").value(votType.xtype); // "unit": "..." (if any) if (tapCol.getUnit() != null && tapCol.getUnit().length() > 0) out.key("unit").value(tapCol.getUnit()); // "ucd": "..." (if any) if (tapCol.getUcd() != null && tapCol.getUcd().length() > 0) out.key("ucd").value(tapCol.getUcd()); // "utype": "..." (if any) if (tapCol.getUtype() != null && tapCol.getUtype().length() > 0) out.key("utype").value(tapCol.getUtype()); // } out.endObject(); } /** * Write the whole data part of the JSON file. * * @param result Result to write. * @param selectedColumns All columns' metadata. * @param out Output stream in which the data 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). * * @throws IOException If there is an error while writing something in the output stream. * @throws InterruptedException If the thread has been interrupted. * @throws JSONException If there is an error while formatting something in JSON. * @throws TAPException If any other error occurs. */ protected void writeData(TableIterator result, DBColumn[] selectedColumns, JSONWriter out, TAPExecutionReport execReport, Thread thread) throws IOException, TAPException, InterruptedException, JSONException{ // [ out.array(); execReport.nbRows = 0; while(result.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()) break; // [ out.array(); int indCol = 0; while(result.hasNextCol()) // ... writeFieldValue(result.nextCol(), selectedColumns[indCol++], out); // ] out.endArray(); execReport.nbRows++; } // ] out.endArray(); } /** * <p>Writes the given field value in JSON and into the given output.</p> * * <p><i>note: special numeric values NaN and Inf (double or float) will be written as NULL values.</i></p> * * @param value The value to write. * @param column The corresponding column metadata. * @param out The stream in which the field value must be written. * * @throws IOException If there is an error while writing the given field value in the given stream. * @throws TAPException If there is any other error (by default: never happen). */ protected void writeFieldValue(final Object value, final DBColumn column, final JSONWriter out) throws IOException, TAPException, JSONException{ if (value instanceof Double && (((Double)value).isNaN() || ((Double)value).isInfinite())) out.value((Object)null); else if (value instanceof Float && (((Float)value).isNaN() || ((Float)value).isInfinite())) out.value((Object)null); else out.value(value); } }