tap.formatter.JSONFormat.java Source code

Java tutorial

Introduction

Here is the source code for tap.formatter.JSONFormat.java

Source

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 Donnes 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&eacute;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);
    }
}